Devolución de funciones en PowerShell y tipos de datos personalizados

En este artículo veremos como se comporta PowerShell a la hora de realizar la devolución de las funciones, ya que no lo hace como los lenguajes de programación "normales", lo que nos puede llevar a confusión y a obtener resultados inesperados e/o indeseados. A parte de ello, a la hora de crear funciones en PowerShell es muy interesante que éstas no devuelvan simple texto, si no que devuelvan objetos tal y como lo hacen los propios Cmdlets de PowerShell, lo que permite que la devolución sea encaminada a otras funciones o Cmdlets de PowerShell. Así pues, es interesante el poder personalizar los tipos de objetos que se devuelven, ya sea quitando y/o añadiendo propiedades a los objetos obtenidos por otros Cmdlets invocados en la función, calculadas en la función o procedentes de otros objetos, o incluso creando nuevos tipos personalizados para la devolución de las funciones.

Las funciones devuelven todo aquello que no sea capturado

Este primer punto va destinado a no tener sorpresas cuando intentamos que una función devuelva algo y el resultado no sea el esperado.

En PowerShell podemos crearnos nuestras propias funciones. Empecemos por una simple, el típico Hola Mundo:

Function  Get-HolaMundo
{
    "Hola Mundo"
}

Si ejecutamos esta función el resultado, obviamente, es que PowerShell muestra por pantalla "Hola Mundo". Si dirigimos su salida a Get-Member, obtendremos como resultado que es de tipo System.String:

PS C:\> Get-HolaMundo
Hola Mundo
PS C:\> Get-HolaMundo|Get-Member


   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object value), int CompareTo(string strB)
Contains         Method                bool Contains(string value)
CopyTo           Method                System.Void CopyTo(int sourceIndex, char[] destination, int destinationIndex,...
EndsWith         Method                bool EndsWith(string value), bool EndsWith(string value, System.StringCompari...
Equals           Method                bool Equals(System.Object obj), bool Equals(string value), bool Equals(string...
GetEnumerator    Method                System.CharEnumerator GetEnumerator()
GetHashCode      Method                int GetHashCode()
GetType          Method                type GetType()
GetTypeCode      Method                System.TypeCode GetTypeCode()
IndexOf          Method                int IndexOf(char value), int IndexOf(char value, int startIndex), int IndexOf...
IndexOfAny       Method                int IndexOfAny(char[] anyOf), int IndexOfAny(char[] anyOf, int startIndex), i...
Insert           Method                string Insert(int startIndex, string value)
IsNormalized     Method                bool IsNormalized(), bool IsNormalized(System.Text.NormalizationForm normaliz...
LastIndexOf      Method                int LastIndexOf(char value), int LastIndexOf(char value, int startIndex), int...
LastIndexOfAny   Method                int LastIndexOfAny(char[] anyOf), int LastIndexOfAny(char[] anyOf, int startI...
Normalize        Method                string Normalize(), string Normalize(System.Text.NormalizationForm normalizat...
PadLeft          Method                string PadLeft(int totalWidth), string PadLeft(int totalWidth, char paddingChar)
PadRight         Method                string PadRight(int totalWidth), string PadRight(int totalWidth, char padding...
Remove           Method                string Remove(int startIndex, int count), string Remove(int startIndex)
Replace          Method                string Replace(char oldChar, char newChar), string Replace(string oldValue, s...
Split            Method                string[] Split(Params char[] separator), string[] Split(char[] separator, int...
StartsWith       Method                bool StartsWith(string value), bool StartsWith(string value, System.StringCom...
Substring        Method                string Substring(int startIndex), string Substring(int startIndex, int length)
ToCharArray      Method                char[] ToCharArray(), char[] ToCharArray(int startIndex, int length)
ToLower          Method                string ToLower(), string ToLower(System.Globalization.CultureInfo culture)
ToLowerInvariant Method                string ToLowerInvariant()
ToString         Method                string ToString(), string ToString(System.IFormatProvider provider)
ToUpper          Method                string ToUpper(), string ToUpper(System.Globalization.CultureInfo culture)
ToUpperInvariant Method                string ToUpperInvariant()
Trim             Method                string Trim(Params char[] trimChars), string Trim()
TrimEnd          Method                string TrimEnd(Params char[] trimChars)
TrimStart        Method                string TrimStart(Params char[] trimChars)
Chars            ParameterizedProperty char Chars(int index) {get;}
Length           Property              System.Int32 Length {get;}

En una función de PowerShell podemos dar así la salida, directamente o usando el comando Return:

Function  Get-HolaMundo
{
    Return "Hola Mundo"
}

El resultado es el mismo. Si modificamos la función anterior para que muestre también un entero:

Function Get-HolaMundo
{
    1
    Return Get-HolaMundo
}

La salida, lógicamente, es una línea con "1" y otra con "Hola Mundo". Su resultado con Get-Member es como sigue:

PS C:\> Get-HolaMundo
1
Hola Mundo
PS C:\> Get-HolaMundo | Get-Member


   TypeName: System.Int32

Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     int CompareTo(System.Object value), int CompareTo(int value)
Equals      Method     bool Equals(System.Object obj), bool Equals(int obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()
ToString    Method     string ToString(), string ToString(string format), string ToString(System.IFormatProvider pro...


   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object value), int CompareTo(string strB)
Contains         Method                bool Contains(string value)
CopyTo           Method                System.Void CopyTo(int sourceIndex, char[] destination, int destinationIndex,...
EndsWith         Method                bool EndsWith(string value), bool EndsWith(string value, System.StringCompari...
Equals           Method                bool Equals(System.Object obj), bool Equals(string value), bool Equals(string...
GetEnumerator    Method                System.CharEnumerator GetEnumerator()
GetHashCode      Method                int GetHashCode()
GetType          Method                type GetType()
GetTypeCode      Method                System.TypeCode GetTypeCode()
IndexOf          Method                int IndexOf(char value), int IndexOf(char value, int startIndex), int IndexOf...
IndexOfAny       Method                int IndexOfAny(char[] anyOf), int IndexOfAny(char[] anyOf, int startIndex), i...
Insert           Method                string Insert(int startIndex, string value)
IsNormalized     Method                bool IsNormalized(), bool IsNormalized(System.Text.NormalizationForm normaliz...
LastIndexOf      Method                int LastIndexOf(char value), int LastIndexOf(char value, int startIndex), int...
LastIndexOfAny   Method                int LastIndexOfAny(char[] anyOf), int LastIndexOfAny(char[] anyOf, int startI...
Normalize        Method                string Normalize(), string Normalize(System.Text.NormalizationForm normalizat...
PadLeft          Method                string PadLeft(int totalWidth), string PadLeft(int totalWidth, char paddingChar)
PadRight         Method                string PadRight(int totalWidth), string PadRight(int totalWidth, char padding...
Remove           Method                string Remove(int startIndex, int count), string Remove(int startIndex)
Replace          Method                string Replace(char oldChar, char newChar), string Replace(string oldValue, s...
Split            Method                string[] Split(Params char[] separator), string[] Split(char[] separator, int...
StartsWith       Method                bool StartsWith(string value), bool StartsWith(string value, System.StringCom...
Substring        Method                string Substring(int startIndex), string Substring(int startIndex, int length)
ToCharArray      Method                char[] ToCharArray(), char[] ToCharArray(int startIndex, int length)
ToLower          Method                string ToLower(), string ToLower(System.Globalization.CultureInfo culture)
ToLowerInvariant Method                string ToLowerInvariant()
ToString         Method                string ToString(), string ToString(System.IFormatProvider provider)
ToUpper          Method                string ToUpper(), string ToUpper(System.Globalization.CultureInfo culture)
ToUpperInvariant Method                string ToUpperInvariant()
Trim             Method                string Trim(Params char[] trimChars), string Trim()
TrimEnd          Method                string TrimEnd(Params char[] trimChars)
TrimStart        Method                string TrimStart(Params char[] trimChars)
Chars            ParameterizedProperty char Chars(int index) {get;}
Length           Property              System.Int32 Length {get;}

Como podemos, ver Get-Member nos muestra dos tipos diferentes de datos como salida System.Int32 y System.String. Si almacenamos la salida en una variable y obtenemos su Get-Member:

PS C:\> $Saludo = Get-HolaMundo
PS C:\> $Saludo | Get-Member


   TypeName: System.Int32

Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     int CompareTo(System.Object value), int CompareTo(int value)
Equals      Method     bool Equals(System.Object obj), bool Equals(int obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()
ToString    Method     string ToString(), string ToString(string format), string ToString(System.IFormatProvider pro...


   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object value), int CompareTo(string strB)
Contains         Method                bool Contains(string value)
CopyTo           Method                System.Void CopyTo(int sourceIndex, char[] destination, int destinationIndex,...
EndsWith         Method                bool EndsWith(string value), bool EndsWith(string value, System.StringCompari...
Equals           Method                bool Equals(System.Object obj), bool Equals(string value), bool Equals(string...
GetEnumerator    Method                System.CharEnumerator GetEnumerator()
GetHashCode      Method                int GetHashCode()
GetType          Method                type GetType()
GetTypeCode      Method                System.TypeCode GetTypeCode()
IndexOf          Method                int IndexOf(char value), int IndexOf(char value, int startIndex), int IndexOf...
IndexOfAny       Method                int IndexOfAny(char[] anyOf), int IndexOfAny(char[] anyOf, int startIndex), i...
Insert           Method                string Insert(int startIndex, string value)
IsNormalized     Method                bool IsNormalized(), bool IsNormalized(System.Text.NormalizationForm normaliz...
LastIndexOf      Method                int LastIndexOf(char value), int LastIndexOf(char value, int startIndex), int...
LastIndexOfAny   Method                int LastIndexOfAny(char[] anyOf), int LastIndexOfAny(char[] anyOf, int startI...
Normalize        Method                string Normalize(), string Normalize(System.Text.NormalizationForm normalizat...
PadLeft          Method                string PadLeft(int totalWidth), string PadLeft(int totalWidth, char paddingChar)
PadRight         Method                string PadRight(int totalWidth), string PadRight(int totalWidth, char padding...
Remove           Method                string Remove(int startIndex, int count), string Remove(int startIndex)
Replace          Method                string Replace(char oldChar, char newChar), string Replace(string oldValue, s...
Split            Method                string[] Split(Params char[] separator), string[] Split(char[] separator, int...
StartsWith       Method                bool StartsWith(string value), bool StartsWith(string value, System.StringCom...
Substring        Method                string Substring(int startIndex), string Substring(int startIndex, int length)
ToCharArray      Method                char[] ToCharArray(), char[] ToCharArray(int startIndex, int length)
ToLower          Method                string ToLower(), string ToLower(System.Globalization.CultureInfo culture)
ToLowerInvariant Method                string ToLowerInvariant()
ToString         Method                string ToString(), string ToString(System.IFormatProvider provider)
ToUpper          Method                string ToUpper(), string ToUpper(System.Globalization.CultureInfo culture)
ToUpperInvariant Method                string ToUpperInvariant()
Trim             Method                string Trim(Params char[] trimChars), string Trim()
TrimEnd          Method                string TrimEnd(Params char[] trimChars)
TrimStart        Method                string TrimStart(Params char[] trimChars)
Chars            ParameterizedProperty char Chars(int index) {get;}
Length           Property              System.Int32 Length {get;}

Como podemos ver, el resultado es exactamente el mismo: System.Int32 y System.String. El haber escrito la función así provoca que devuelva un array cuyo primer elemento es 1 y su segundo elemento es Hola Mundo, cada elemento tiene su propio tipo y por tanto sus propias propiedades y métodos. Lo vemos a continuación, como accedemos a 1 como elemento cero del array, cómo accedemos a Hola Mundo como elemento uno del array y como el método ToUpper de la clase System.String funciona con el elemento uno y da error con el cero:

PS C:\Lab> $Saludo[0]
1
PS C:\> $Saludo[1]
Hola Mundo
PS C:\> $Saludo[1].ToUpper()
HOLA MUNDO
PS C:\> $Saludo[0].ToUpper()
Error en la invocación del método porque [System.Int32] no contiene ningún método llamado 'ToUpper'.
En línea: 1 Carácter: 19
+ $Saludo[0].ToUpper <<<< ()
    + CategoryInfo          : InvalidOperation: (ToUpper:String) [], RuntimeException
    + FullyQualifiedErrorId : MethodNotFound

Que la devolución de la función sea así chocará a cualquier programador, pues al ver un Return se pensará que el resultado debiera ser que la variable hubiese almacenado el saludo y mostrado por pantalla el número. Como reza el título de este punto, una función de PowerShell devuelve todo aquello que no sea interceptado. De hecho, la instrucción Return no devuelve un valor, si no que lo que hace es salir de la función por lo que estas dos definiciones de la función se comportan exactamente igual:

Function Get-HolaMundo
{
    Return "Hola Mundo"
}
Function Get-HolaMundo
{
    "Hola Mundo"
    Return
}

Es decir que la línea Return "Hola Mundo" es ejecutada por PowerShell como una línea "Hola Mundo" seguida de otra Return.

¿Cómo podemos entonces hacer que la función se comporte de la misma manera que una función de un lenguaje de programación "normal"? La respuesta es interceptando todo aquello que no queremos que sea parte de la devolución. Volvamos pues a la variante de la función en la que se muestra también un entero y lo interceptaremos con una variable:

PS C:\> Function Get-HolaMundo
{
    $Entero = 1
    Return "Hola Mundo"
}

Cuando ejecutamos la función se mostrará sólo el saludo, y se habrá creado una variable $Entero que tendrá de valor "1" y que será destruida al salir de la función, por lo que si consultamos su valor, éste será nulo, habrá desaparecido. Si pasamos la función a Get-Member veremos cómo es una devolución System.String únicamente, cómo podemos acceder directamente al método ToUpper (sin poner índice de array) y cómo $Entero pasado a Get-Member da un error, pues ya no existe:

PS C:\> $Saludo = Get-HolaMundo
PS C:\> $Saludo | Get-Member


   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object value), int CompareTo(string strB)
Contains         Method                bool Contains(string value)
CopyTo           Method                System.Void CopyTo(int sourceIndex, char[] destination, int destinationIndex,...
EndsWith         Method                bool EndsWith(string value), bool EndsWith(string value, System.StringCompari...
Equals           Method                bool Equals(System.Object obj), bool Equals(string value), bool Equals(string...
GetEnumerator    Method                System.CharEnumerator GetEnumerator()
GetHashCode      Method                int GetHashCode()
GetType          Method                type GetType()
GetTypeCode      Method                System.TypeCode GetTypeCode()
IndexOf          Method                int IndexOf(char value), int IndexOf(char value, int startIndex), int IndexOf...
IndexOfAny       Method                int IndexOfAny(char[] anyOf), int IndexOfAny(char[] anyOf, int startIndex), i...
Insert           Method                string Insert(int startIndex, string value)
IsNormalized     Method                bool IsNormalized(), bool IsNormalized(System.Text.NormalizationForm normaliz...
LastIndexOf      Method                int LastIndexOf(char value), int LastIndexOf(char value, int startIndex), int...
LastIndexOfAny   Method                int LastIndexOfAny(char[] anyOf), int LastIndexOfAny(char[] anyOf, int startI...
Normalize        Method                string Normalize(), string Normalize(System.Text.NormalizationForm normalizat...
PadLeft          Method                string PadLeft(int totalWidth), string PadLeft(int totalWidth, char paddingChar)
PadRight         Method                string PadRight(int totalWidth), string PadRight(int totalWidth, char padding...
Remove           Method                string Remove(int startIndex, int count), string Remove(int startIndex)
Replace          Method                string Replace(char oldChar, char newChar), string Replace(string oldValue, s...
Split            Method                string[] Split(Params char[] separator), string[] Split(char[] separator, int...
StartsWith       Method                bool StartsWith(string value), bool StartsWith(string value, System.StringCom...
Substring        Method                string Substring(int startIndex), string Substring(int startIndex, int length)
ToCharArray      Method                char[] ToCharArray(), char[] ToCharArray(int startIndex, int length)
ToLower          Method                string ToLower(), string ToLower(System.Globalization.CultureInfo culture)
ToLowerInvariant Method                string ToLowerInvariant()
ToString         Method                string ToString(), string ToString(System.IFormatProvider provider)
ToUpper          Method                string ToUpper(), string ToUpper(System.Globalization.CultureInfo culture)
ToUpperInvariant Method                string ToUpperInvariant()
Trim             Method                string Trim(Params char[] trimChars), string Trim()
TrimEnd          Method                string TrimEnd(Params char[] trimChars)
TrimStart        Method                string TrimStart(Params char[] trimChars)
Chars            ParameterizedProperty char Chars(int index) {get;}
Length           Property              System.Int32 Length {get;}


PS C:\> $Saludo.ToUpper()
HOLA MUNDO
PS C:\> $Entero
PS C:\> $Entero | Get-Member
Get-Member : No se ha especificado ningún objeto para el cmdlet get-member.
En línea: 1 Carácter: 21
+ $Entero | Get-Member <<<<
    + CategoryInfo          : CloseError: (:) [Get-Member], InvalidOperationException
    + FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand

¿Cómo podemos hacer que la función tenga como devolución lo que queremos pero que a su vez presente información, según se va ejecutando, que no sea parte de la devolución? En nuestra ayuda viene el Cmdlet Write-Host, que hace eso precísamente, enviar información a la pantalla y que ésta no sea considerada ninguna devolución (además de permitir establecer color de fondo y letra, entre otras cosas):

Function Get-HolaMundo
{
    Write-Host 1
    Return "Hola Mundo"
}

Ahora la función devuelve lo que se desea, System.String y muestra el número:

PS C:\> $Saludo = Get-HolaMundo
1
PS C:\> $Saludo | Get-Member


   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object value), int CompareTo(string strB)
Contains         Method                bool Contains(string value)
CopyTo           Method                System.Void CopyTo(int sourceIndex, char[] destination, int destinationIndex,...
EndsWith         Method                bool EndsWith(string value), bool EndsWith(string value, System.StringCompari...
Equals           Method                bool Equals(System.Object obj), bool Equals(string value), bool Equals(string...
GetEnumerator    Method                System.CharEnumerator GetEnumerator()
GetHashCode      Method                int GetHashCode()
GetType          Method                type GetType()
GetTypeCode      Method                System.TypeCode GetTypeCode()
IndexOf          Method                int IndexOf(char value), int IndexOf(char value, int startIndex), int IndexOf...
IndexOfAny       Method                int IndexOfAny(char[] anyOf), int IndexOfAny(char[] anyOf, int startIndex), i...
Insert           Method                string Insert(int startIndex, string value)
IsNormalized     Method                bool IsNormalized(), bool IsNormalized(System.Text.NormalizationForm normaliz...
LastIndexOf      Method                int LastIndexOf(char value), int LastIndexOf(char value, int startIndex), int...
LastIndexOfAny   Method                int LastIndexOfAny(char[] anyOf), int LastIndexOfAny(char[] anyOf, int startI...
Normalize        Method                string Normalize(), string Normalize(System.Text.NormalizationForm normalizat...
PadLeft          Method                string PadLeft(int totalWidth), string PadLeft(int totalWidth, char paddingChar)
PadRight         Method                string PadRight(int totalWidth), string PadRight(int totalWidth, char padding...
Remove           Method                string Remove(int startIndex, int count), string Remove(int startIndex)
Replace          Method                string Replace(char oldChar, char newChar), string Replace(string oldValue, s...
Split            Method                string[] Split(Params char[] separator), string[] Split(char[] separator, int...
StartsWith       Method                bool StartsWith(string value), bool StartsWith(string value, System.StringCom...
Substring        Method                string Substring(int startIndex), string Substring(int startIndex, int length)
ToCharArray      Method                char[] ToCharArray(), char[] ToCharArray(int startIndex, int length)
ToLower          Method                string ToLower(), string ToLower(System.Globalization.CultureInfo culture)
ToLowerInvariant Method                string ToLowerInvariant()
ToString         Method                string ToString(), string ToString(System.IFormatProvider provider)
ToUpper          Method                string ToUpper(), string ToUpper(System.Globalization.CultureInfo culture)
ToUpperInvariant Method                string ToUpperInvariant()
Trim             Method                string Trim(Params char[] trimChars), string Trim()
TrimEnd          Method                string TrimEnd(Params char[] trimChars)
TrimStart        Method                string TrimStart(Params char[] trimChars)
Chars            ParameterizedProperty char Chars(int index) {get;}
Length           Property              System.Int32 Length {get;}


PS C:\> $Saludo.ToUpper()
HOLA MUNDO

Como se ve, al enviar la función a Get-Member, se ha mostrado por pantalla el número "1", pero no ha sido contemplado por Get-Member, que sólo ha evaluado el mensaje (ni que decir tiene que la definición de la función habría dado el mismo resultado si en lugar de Return "Hola Mundo" hubiera puesto sólo "Hola Mundo"). Por tanto el uso de Return sólo es imprescindible cuando en una función necesitamos salir antes de su final en un control de flujo, por ejemplo un If dentro de un For. Usar Return para la devolución de la función es sólo una cuestión de convención de código, ya que facilita su lectura, cuando la devolución se produce al final de la función.

¿Por qué he soltado este ladrillo? Es algo que tendremos que tener en cuenta a la hora de hacer funciones y que de no hacerlo, cuando desarrollemos funciones nos podrá despistar, al no ser capaces de acceder a los métodos y propiedades de los elementos devueltos por las funciones por no tener en cuenta que la devolución de la función es un array "variopinto" y no un único tipo de dato.

Devolucion de funciones con tipos personalizados

Introducción

Una de las prestaciones más importantes de PowerShell y que la diferencian de cualquier otro Shell (ya sean shells de Unix/Linux, como BASH, o el propio CMD de Windows), es que en lugar de devolver texto devuelve objetos, lo que hace que las canalizaciones sean muy poderosas, pues permiten que se puedan procesar en función de las propiedades de los objetos devueltos y no de la devolucion en sí. Por ejemplo un simple Get-ChildItem sobre un directorio nos devolverá un listado similar al que nos devuelve el DIR de CMD. Si en CMD hacemos DIR:

C:\>dir
 El volumen de la unidad C es Sistema
 El número de serie del volumen es: F85B-4E6D

 Directorio de C:\

19.01.2010  16:05                 0 a.txt
09.12.2009  23:59    <DIR>          aeee88676f12056b7a7bcabaf65d83
01.03.2010  14:58    <DIR>          Archivos de programa
12.11.2009  16:41                 0 AUTOEXEC.BAT
19.01.2010  16:05                 0 b.txt
17.03.2010  13:00                18 bat.bat
09.02.2010  10:38                12 c.txt
12.11.2009  16:41                 0 CONFIG.SYS
12.11.2009  16:51    <DIR>          Documents and Settings
14.12.2009  10:31             4.760 msinfo.txt
19.01.2010  12:34               875 oab.bat
03.02.2010  16:13             6.843 pepe.txt
18.02.2010  16:51    <DIR>          Pruebas
10.12.2009  11:27             3.351 software.tab
19.02.2010  09:06             4.023 software.tsv
03.03.2010  16:27                32 usuarios.tab
22.03.2010  10:30    <DIR>          WINDOWS
              12 archivos         19.914 bytes
               5 dirs  47.203.975.168 bytes libres

Si la salida de DIR la queremos encaminar a otro comando, por ejemplo SORT, esta salida es tratada como texto y el resultado que obtendremos será:

C:\>dir |sort


               5 dirs  47.203.975.168 bytes libres
              12 archivos         19.914 bytes
 Directorio de C:\
 El número de serie del volumen es: F85B-4E6D
 El volumen de la unidad C es Sistema
01.03.2010  14:58    <DIR>          Archivos de programa
03.02.2010  16:13             6.843 pepe.txt
03.03.2010  16:27                32 usuarios.tab
09.02.2010  10:38                12 c.txt
09.12.2009  23:59    <DIR>          aeee88676f12056b7a7bcabaf65d83
10.12.2009  11:27             3.351 software.tab
12.11.2009  16:41                 0 AUTOEXEC.BAT
12.11.2009  16:41                 0 CONFIG.SYS
12.11.2009  16:51    <DIR>          Documents and Settings
14.12.2009  10:31             4.760 msinfo.txt
17.03.2010  13:00                18 bat.bat
18.02.2010  16:51    <DIR>          Pruebas
19.01.2010  12:34               875 oab.bat
19.01.2010  16:05                 0 a.txt
19.01.2010  16:05                 0 b.txt
19.02.2010  09:06             4.023 software.tsv
22.03.2010  10:30    <DIR>          WINDOWS

Como se puede ver, se ha ordenado en función de cómo empieza cada línea, por ello lo primero que ha puesto son las dos líneas en blanco que hay en la salida original (una después de la línea con el número de serie y otra después del nombre del directorio listado). Después las dos líneas que empiezan con espacio en blanco, ordenadas por el número por el que empiezan. A continuación las líneas ordenadas según el texto, de hecho las fechas no aparecen ordenadas como fechas, si no como texto, apareciendo el uno de Marzo antes que el tres de Febrero, por ejemplo. En Unix/Linux, hay aplicativos que permiten más operaciones, como GREP o AWK, pero a la hora de la verdad siguen trabajando con el texto que devuelven los comandos.

PowerShell devuelve objetos, de manera que podemos tratar con ellos a nuestra conveniencia, independientemente de lo que se muestre por pantalla. Por ejemplo. Get-ChildItem nos devuelve:

PS C:\> Get-ChildItem


    Directorio: C:\


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----         9.12.2009     23:59            aeee88676f12056b7a7bcabaf65d83
d-r--          1.3.2010     14:58            Archivos de programa
d----        12.11.2009     16:51            Documents and Settings
d----         18.2.2010     16:51            Pruebas
d----         22.3.2010     10:30            WINDOWS
-a---         19.1.2010     16:05          0 a.txt
-a---        12.11.2009     16:41          0 AUTOEXEC.BAT
-a---         19.1.2010     16:05          0 b.txt
-a---         17.3.2010     13:00         18 bat.bat
-a---          9.2.2010     10:38         12 c.txt
-a---        12.11.2009     16:41          0 CONFIG.SYS
-a---        14.12.2009     10:31       4760 msinfo.txt
-a---         19.1.2010     12:34        875 oab.bat
-a---          3.2.2010     16:13       6843 pepe.txt
-a---        10.12.2009     11:27       3351 software.tab
-a---         19.2.2010      9:06       4023 software.tsv
-a---          3.3.2010     16:27         32 usuarios.tab

Si encaminamos la salida a Sort-Object, podemos ordenar según la fecha de creación del fichero, cosa que no ha sido mostrada en pantalla:

PS C:\> Get-ChildItem | Sort-Object CreationTime


    Directorio: C:\


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        12.11.2009     16:51            Documents and Settings
d-r--          1.3.2010     14:58            Archivos de programa
-a---        12.11.2009     16:41          0 CONFIG.SYS
-a---        12.11.2009     16:41          0 AUTOEXEC.BAT
d----         22.3.2010     10:30            WINDOWS
d----         9.12.2009     23:59            aeee88676f12056b7a7bcabaf65d83
-a---        10.12.2009     11:27       3351 software.tab
-a---        14.12.2009     10:31       4760 msinfo.txt
-a---          3.2.2010     16:13       6843 pepe.txt
-a---         19.1.2010     12:34        875 oab.bat
-a---         19.1.2010     16:05          0 b.txt
-a---         19.1.2010     16:05          0 a.txt
d----         18.2.2010     16:51            Pruebas
-a---          9.2.2010     10:38         12 c.txt
-a---         19.2.2010      9:06       4023 software.tsv
-a---          3.3.2010     16:27         32 usuarios.tab
-a---         17.3.2010     13:00         18 bat.bat

Como vemos el orden ha cambiado y los encabezados del listado no se han mezclado con los resultados, como sí pasó con SORT al recibir la salida de DIR ¡y además ni siquiera se está mostrando el campo por el que hemos ordenado! Esto es gracias a que con Sort-Object le hemos especificado la propiedad del objeto fichero en la que se basa el ordenamiento del listado, mientras que la forma predeterminada de devolver el listado no muestra esa propiedad. Podemos hacer que se muestre con Select-Object o con Format-Table o Format-List si lo necesitamos.

Si a la salida de Get-ChildItem la encaminamos a Get-Member, podremos ver que la devolución, lejos de ser texto, es de dos tipos de objeto, carpetas y ficheros:

PS C:\> Get-ChildItem | Get-Member


   TypeName: System.IO.DirectoryInfo

Name                      MemberType     Definition
----                      ----------     ----------
Mode                      CodeProperty   System.String Mode{get=Mode;}
Create                    Method         System.Void Create(System.Security.AccessControl.DirectorySecurity director...
CreateObjRef              Method         System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
CreateSubdirectory        Method         System.IO.DirectoryInfo CreateSubdirectory(string path), System.IO.Director...
Delete                    Method         System.Void Delete(), System.Void Delete(bool recursive)
Equals                    Method         bool Equals(System.Object obj)
GetAccessControl          Method         System.Security.AccessControl.DirectorySecurity GetAccessControl(), System....
GetDirectories            Method         System.IO.DirectoryInfo[] GetDirectories(string searchPattern), System.IO.D...
GetFiles                  Method         System.IO.FileInfo[] GetFiles(string searchPattern), System.IO.FileInfo[] G...
GetFileSystemInfos        Method         System.IO.FileSystemInfo[] GetFileSystemInfos(string searchPattern), System...
GetHashCode               Method         int GetHashCode()
GetLifetimeService        Method         System.Object GetLifetimeService()
GetObjectData             Method         System.Void GetObjectData(System.Runtime.Serialization.SerializationInfo in...
GetType                   Method         type GetType()
InitializeLifetimeService Method         System.Object InitializeLifetimeService()
MoveTo                    Method         System.Void MoveTo(string destDirName)
Refresh                   Method         System.Void Refresh()
SetAccessControl          Method         System.Void SetAccessControl(System.Security.AccessControl.DirectorySecurit...
ToString                  Method         string ToString()
PSChildName               NoteProperty   System.String PSChildName=aeee88676f12056b7a7bcabaf65d83
PSDrive                   NoteProperty   System.Management.Automation.PSDriveInfo PSDrive=C
PSIsContainer             NoteProperty   System.Boolean PSIsContainer=True
PSParentPath              NoteProperty   System.String PSParentPath=Microsoft.PowerShell.Core\FileSystem::C:\
PSPath                    NoteProperty   System.String PSPath=Microsoft.PowerShell.Core\FileSystem::C:\aeee88676f120...
PSProvider                NoteProperty   System.Management.Automation.ProviderInfo PSProvider=Microsoft.PowerShell.C...
Attributes                Property       System.IO.FileAttributes Attributes {get;set;}
CreationTime              Property       System.DateTime CreationTime {get;set;}
CreationTimeUtc           Property       System.DateTime CreationTimeUtc {get;set;}
Exists                    Property       System.Boolean Exists {get;}
Extension                 Property       System.String Extension {get;}
FullName                  Property       System.String FullName {get;}
LastAccessTime            Property       System.DateTime LastAccessTime {get;set;}
LastAccessTimeUtc         Property       System.DateTime LastAccessTimeUtc {get;set;}
LastWriteTime             Property       System.DateTime LastWriteTime {get;set;}
LastWriteTimeUtc          Property       System.DateTime LastWriteTimeUtc {get;set;}
Name                      Property       System.String Name {get;}
Parent                    Property       System.IO.DirectoryInfo Parent {get;}
Root                      Property       System.IO.DirectoryInfo Root {get;}
BaseName                  ScriptProperty System.Object BaseName {get=$this.Name;}


   TypeName: System.IO.FileInfo

Name                      MemberType     Definition
----                      ----------     ----------
Mode                      CodeProperty   System.String Mode{get=Mode;}
AppendText                Method         System.IO.StreamWriter AppendText()
CopyTo                    Method         System.IO.FileInfo CopyTo(string destFileName), System.IO.FileInfo CopyTo(s...
Create                    Method         System.IO.FileStream Create()
CreateObjRef              Method         System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
CreateText                Method         System.IO.StreamWriter CreateText()
Decrypt                   Method         System.Void Decrypt()
Delete                    Method         System.Void Delete()
Encrypt                   Method         System.Void Encrypt()
Equals                    Method         bool Equals(System.Object obj)
GetAccessControl          Method         System.Security.AccessControl.FileSecurity GetAccessControl(), System.Secur...
GetHashCode               Method         int GetHashCode()
GetLifetimeService        Method         System.Object GetLifetimeService()
GetObjectData             Method         System.Void GetObjectData(System.Runtime.Serialization.SerializationInfo in...
GetType                   Method         type GetType()
InitializeLifetimeService Method         System.Object InitializeLifetimeService()
MoveTo                    Method         System.Void MoveTo(string destFileName)
Open                      Method         System.IO.FileStream Open(System.IO.FileMode mode), System.IO.FileStream Op...
OpenRead                  Method         System.IO.FileStream OpenRead()
OpenText                  Method         System.IO.StreamReader OpenText()
OpenWrite                 Method         System.IO.FileStream OpenWrite()
Refresh                   Method         System.Void Refresh()
Replace                   Method         System.IO.FileInfo Replace(string destinationFileName, string destinationBa...
SetAccessControl          Method         System.Void SetAccessControl(System.Security.AccessControl.FileSecurity fil...
ToString                  Method         string ToString()
PSChildName               NoteProperty   System.String PSChildName=a.txt
PSDrive                   NoteProperty   System.Management.Automation.PSDriveInfo PSDrive=C
PSIsContainer             NoteProperty   System.Boolean PSIsContainer=False
PSParentPath              NoteProperty   System.String PSParentPath=Microsoft.PowerShell.Core\FileSystem::C:\
PSPath                    NoteProperty   System.String PSPath=Microsoft.PowerShell.Core\FileSystem::C:\a.txt
PSProvider                NoteProperty   System.Management.Automation.ProviderInfo PSProvider=Microsoft.PowerShell.C...
Attributes                Property       System.IO.FileAttributes Attributes {get;set;}
CreationTime              Property       System.DateTime CreationTime {get;set;}
CreationTimeUtc           Property       System.DateTime CreationTimeUtc {get;set;}
Directory                 Property       System.IO.DirectoryInfo Directory {get;}
DirectoryName             Property       System.String DirectoryName {get;}
Exists                    Property       System.Boolean Exists {get;}
Extension                 Property       System.String Extension {get;}
FullName                  Property       System.String FullName {get;}
IsReadOnly                Property       System.Boolean IsReadOnly {get;set;}
LastAccessTime            Property       System.DateTime LastAccessTime {get;set;}
LastAccessTimeUtc         Property       System.DateTime LastAccessTimeUtc {get;set;}
LastWriteTime             Property       System.DateTime LastWriteTime {get;set;}
LastWriteTimeUtc          Property       System.DateTime LastWriteTimeUtc {get;set;}
Length                    Property       System.Int64 Length {get;}
Name                      Property       System.String Name {get;}
BaseName                  ScriptProperty System.Object BaseName {get=if ($this.Extension.Length -gt 0){$this.Name.Re...
VersionInfo               ScriptProperty System.Object VersionInfo {get=[System.Diagnostics.FileVersionInfo]::GetVer...

Podemos personalizar el listado por medio de Format-Table o Format-List o incluso Select-Object. Pongamos que queremos un listado igual al predeterminado pero mostrando la fecha de creación en lugar de la de última modificación:

PS C:\> Get-ChildItem | Sort-Object CreationTime | Format-Table Mode,CreationTime,Length,Name -Autosize

Mode  CreationTime        Length Name
----  ------------        ------ ----
d---- 12.11.2009 16:18:56        Documents and Settings
d-r-- 12.11.2009 16:19:47        Archivos de programa
-a--- 12.11.2009 16:41:30 0      CONFIG.SYS
-a--- 12.11.2009 16:41:30 0      AUTOEXEC.BAT
d---- 12.11.2009 17:15:36        WINDOWS
d---- 9.12.2009 23:58:57         aeee88676f12056b7a7bcabaf65d83
-a--- 10.12.2009 11:26:20 3351   software.tab
-a--- 14.12.2009 10:31:20 4760   msinfo.txt
-a--- 14.12.2009 13:05:56 6843   pepe.txt
-a--- 19.1.2010 12:16:14  875    oab.bat
-a--- 19.1.2010 16:05:05  0      b.txt
-a--- 19.1.2010 16:05:05  0      a.txt
d---- 1.2.2010 12:08:52          Pruebas
-a--- 9.2.2010 10:38:01   12     c.txt
-a--- 19.2.2010 9:02:08   4023   software.tsv
-a--- 3.3.2010 16:27:55   32     usuarios.tab
-a--- 17.3.2010 12:58:26  18     bat.bat

¿Perfecto, verdad? Pues.... casi. El problema de esto es que hemos perdido los tipos de devolución, ya que Format-List o Format-Table devuelven tipos de datos propios de Format:

PS C:\> Get-ChildItem | Format-Table | Get-Member


   TypeName: Microsoft.PowerShell.Commands.Internal.Format.FormatStartData

Name                                    MemberType Definition
----                                    ---------- ----------
Equals                                  Method     bool Equals(System.Object obj)
GetHashCode                             Method     int GetHashCode()
GetType                                 Method     type GetType()
ToString                                Method     string ToString()
autosizeInfo                            Property   Microsoft.PowerShell.Commands.Internal.Format.AutosizeInfo autosi...
ClassId2e4f51ef21dd47e99d3c952918aff9cd Property   System.String ClassId2e4f51ef21dd47e99d3c952918aff9cd {get;}
groupingEntry                           Property   Microsoft.PowerShell.Commands.Internal.Format.GroupingEntry group...
pageFooterEntry                         Property   Microsoft.PowerShell.Commands.Internal.Format.PageFooterEntry pag...
pageHeaderEntry                         Property   Microsoft.PowerShell.Commands.Internal.Format.PageHeaderEntry pag...
shapeInfo                               Property   Microsoft.PowerShell.Commands.Internal.Format.ShapeInfo shapeInfo...


   TypeName: Microsoft.PowerShell.Commands.Internal.Format.GroupStartData

Name                                    MemberType Definition
----                                    ---------- ----------
Equals                                  Method     bool Equals(System.Object obj)
GetHashCode                             Method     int GetHashCode()
GetType                                 Method     type GetType()
ToString                                Method     string ToString()
ClassId2e4f51ef21dd47e99d3c952918aff9cd Property   System.String ClassId2e4f51ef21dd47e99d3c952918aff9cd {get;}
groupingEntry                           Property   Microsoft.PowerShell.Commands.Internal.Format.GroupingEntry group...
shapeInfo                               Property   Microsoft.PowerShell.Commands.Internal.Format.ShapeInfo shapeInfo...


   TypeName: Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData

Name                                    MemberType Definition
----                                    ---------- ----------
Equals                                  Method     bool Equals(System.Object obj)
GetHashCode                             Method     int GetHashCode()
GetType                                 Method     type GetType()
ToString                                Method     string ToString()
ClassId2e4f51ef21dd47e99d3c952918aff9cd Property   System.String ClassId2e4f51ef21dd47e99d3c952918aff9cd {get;}
formatEntryInfo                         Property   Microsoft.PowerShell.Commands.Internal.Format.FormatEntryInfo for...
outOfBand                               Property   System.Boolean outOfBand {get;set;}
writeErrorStream                        Property   System.Boolean writeErrorStream {get;set;}


   TypeName: Microsoft.PowerShell.Commands.Internal.Format.GroupEndData

Name                                    MemberType Definition
----                                    ---------- ----------
Equals                                  Method     bool Equals(System.Object obj)
GetHashCode                             Method     int GetHashCode()
GetType                                 Method     type GetType()
ToString                                Method     string ToString()
ClassId2e4f51ef21dd47e99d3c952918aff9cd Property   System.String ClassId2e4f51ef21dd47e99d3c952918aff9cd {get;}
groupingEntry                           Property   Microsoft.PowerShell.Commands.Internal.Format.GroupingEntry group...


   TypeName: Microsoft.PowerShell.Commands.Internal.Format.FormatEndData

Name                                    MemberType Definition
----                                    ---------- ----------
Equals                                  Method     bool Equals(System.Object obj)
GetHashCode                             Method     int GetHashCode()
GetType                                 Method     type GetType()
ToString                                Method     string ToString()
ClassId2e4f51ef21dd47e99d3c952918aff9cd Property   System.String ClassId2e4f51ef21dd47e99d3c952918aff9cd {get;}
groupingEntry                           Property   Microsoft.PowerShell.Commands.Internal.Format.GroupingEntry group...

Como se puede ver, hemos perdido las clases fichero o carpeta, que han sido sutituidas por clases propias de la devolución de Format-Table. Esto implica que Format-Table sólo deberíamos utilizarlo cuando queramos mostrar el resultado final, pero nunca como devolución de una función, pues esta devolución ya no será encaminable.

Pongamos un ejemplo. Supongamos que queremos que el tamaño del fichero sea expresado en Kilobytes en lugar de Bytes. Si lo mostrado por Format-Table es el resultado final, podría ser que nos valiera con poner una columna calculada para que se mostrase:

PS C:\> Get-ChildItem | Format-Table Mode,LastWriteTime, `
>> @{Label="Tamaño (KB)";Expression={$_.Length / 1kb};Alignment="Right";FormatString="N2"}, `
>> Name -Autosize
>>

Mode  LastWriteTime       Tamaño (KB) Name
----  -------------       ----------- ----
d---- 9.12.2009 23:59:32         0,00 aeee88676f12056b7a7bcabaf65d83
d-r-- 1.3.2010 14:58:31          0,00 Archivos de programa
d---- 12.11.2009 16:51:28        0,00 Documents and Settings
d---- 18.2.2010 16:51:16         0,00 Pruebas
d---- 22.3.2010 10:30:17         0,00 WINDOWS
-a--- 19.1.2010 16:05:05         0,00 a.txt
-a--- 12.11.2009 16:41:30        0,00 AUTOEXEC.BAT
-a--- 19.1.2010 16:05:12         0,00 b.txt
-a--- 17.3.2010 13:00:04         0,02 bat.bat
-a--- 9.2.2010 10:38:26          0,01 c.txt
-a--- 12.11.2009 16:41:30        0,00 CONFIG.SYS
-a--- 14.12.2009 10:31:20        4,65 msinfo.txt
-a--- 19.1.2010 12:34:57         0,85 oab.bat
-a--- 3.2.2010 16:13:44          6,68 pepe.txt
-a--- 10.12.2009 11:27:53        3,27 software.tab
-a--- 19.2.2010 9:06:11          3,93 software.tsv
-a--- 3.3.2010 16:27:57          0,03 usuarios.tab

Para poner una columna calculada a Format-Table, se debe definir como un hastable, cuyos posibles campos son:

En el ejemplo anterior pusimos de etiqueta "Tamaño (KB)", de expresión "$_.Length / 1kb", de alineación "Right" y de formato "N2" (número con separadores de miles y dos decimales).Esta información nos puede resultar útil si es la que queremos, pero no nos permite encaminar la salida a otro comando, por ejemplo para ordenar o filtrar. Supongamos que queremos crear una función personalizada para que nos muestre lo mismo que Get-ChildItem, pero expresando el tamaño en KBytes. Queremos que devuelva los objetos, y por tanto no nos vale con que la función termine haciendo un Format-Table. En nuestra ayuda aparecen tres instrucciones:

Con ellas podremos personalizar objetos existentes, agregando propiedades a los mismos (principalmente Select-Object y Add-Member, de forma más compleja Add-Type) e incluso crear nuevos tipos de objetos (en parte Select-Object y Add-Member, y sobre todo Add-Type).

Personalización de tipos de objetos existentes

Como hemos visto antes, se pueden personalizar objetos existentes agregando propiedades o métodos que no tienen, de forma que cumplan con nuestras expectativas. Esto se puede hacer con Select-Object y Add-Member (también con Add-Type, si bien haremos referencia a esto como parte de la descripción de Add-Type en la sección de creación de nuevos tipos de objetos).

Select-Object

Para que una función nuestra devuelva un objeto "original" al que hemos agregado una o más propiedades, podemos usar Select-Object. Veamos esta función:

Function Get-MyChildItem($Ruta="$((PWD).Path)")
{
    Write-Host "`n`rDirectorio $Ruta" 
    $Listado = Get-ChildItem -Path $Ruta
    ForEach($Elemento in $Listado)
    {
        $Elemento = $Elemento |Select-Object Mode,Name,Length,LastWriteTime,`
                                             LengthKB
        $Elemento.LengthKB = "{0:N2}" -f ($Elemento.Length / 1kb)
        $Elemento
    }
}

Hemos definido una función Get-MyChildItem que recibe una ruta, que será la ruta en la que está el inductor de PowerShell si este parámetro es omitido. La función realiza una llamada a Get-ChildItem y le pasa la ruta establecida como parámetro. Una vez obtenido el listado, lo recorre elemento a elemento. Fijémonos en la línea:

$Elemento = $Elemento |Select-Object Mode,Name,Length,LastWriteTime,`
                                             LengthKB

Esta línea está asignando al elemento en curso a sí mismo, pero filtrando sus propiedades con Select-Object para que conserve las propiedades Mode, Name, LastWriteTime y Length; además, le agrega la propiedad LengthKB, que no existe en un objeto de tipo directorio o fichero. Después de ésto, asigna a la propiedad así agregada el valor de la misma (el resto conserva el valor que tenía el objeto original); en este caso el valor asignado es el resutado de expresar en KiloBytes el tamaño, con formato numérico de dos decimales:

$Elemento.LengthKB = "{0:N2}" -f ($Elemento.Length / 1kb)

Finalmente, la función devuelve el objeto listado (recordemos el primer punto de este artículo, en el que se explica porqué se muestra el nombre del directorio con Write-Host, para que no forme parte de la función y tengamos en ésta sólo directorios y ficheros). Si ahora vemos qué compone la devolución de esta función:

PS C:\> Get-MyChildItem | Get-Member

Directorio C:\


   TypeName: Selected.System.IO.DirectoryInfo

Name          MemberType   Definition
----          ----------   ----------
Equals        Method       bool Equals(System.Object obj)
GetHashCode   Method       int GetHashCode()
GetType       Method       type GetType()
ToString      Method       string ToString()
LastWriteTime NoteProperty System.DateTime LastWriteTime=9.12.2009 23:59:32
Length        NoteProperty  Length=null
LengthKB      NoteProperty System.String LengthKB=0,00
Mode          NoteProperty System.String Mode=d----
Name          NoteProperty System.String Name=aeee88676f12056b7a7bcabaf65d83


   TypeName: Selected.System.IO.FileInfo

Name          MemberType   Definition
----          ----------   ----------
Equals        Method       bool Equals(System.Object obj)
GetHashCode   Method       int GetHashCode()
GetType       Method       type GetType()
ToString      Method       string ToString()
LastWriteTime NoteProperty System.DateTime LastWriteTime=19.1.2010 16:05:05
Length        NoteProperty System.Int64 Length=0
LengthKB      NoteProperty System.String LengthKB=0,00
Mode          NoteProperty System.String Mode=-a---
Name          NoteProperty System.String Name=a.txt

Podemos comprobar que la devolución se sigue componiendo de carpetas y ficheros, pero ¡oh, algo ha pasado! Si observamos, tanto las carpetas como los ficheros han visto reducidas sus propiedades drásticamente a aquellas que especificamos con Select-Object más los métodos propios de todo objeto en PowerShell. De hecho, hay otra cosa que nos da una pista de que ha cambiado el tipo de objeto, a pesar de que Get-Member diga lo contrario. Si ejecutamos Get-ChildItem sin especificar el formato de salida, ésta se produce con el formato predeterminado de los tipos de objeto File y Directory, que es el formato de tabla:

PS C:\> get-childitem


    Directorio: C:\Documents and Settings\Lab


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d---s        26/04/2010      8:43            Cookies
d----        17/03/2010     15:22            Escritorio
d-r--        12/11/2009     16:52            Favoritos
d-r--        12/11/2009     16:19            Menú Inicio
d-r--        21/04/2010     11:20            Mis documentos
d---s        19/11/2009     23:32            UserData
-a---        16/04/2010     11:44         46 a.txt
-a---        16/04/2010     11:47         44 b.txt

Sin embargo nuestra función Get-MyChildItem nos da la devolución con formato de lista:

PS C:\> get-mychilditem

Directorio C:\Documents and Settings\Lab


Mode          : d---s
Name          : Cookies
Length        :
LastWriteTime : 26/04/2010 8:43:34
LengthKB      : 0,00

Mode          : d----
Name          : Escritorio
Length        :
LastWriteTime : 17/03/2010 15:22:47
LengthKB      : 0,00

Mode          : d-r--
Name          : Favoritos
Length        :
LastWriteTime : 12/11/2009 16:52:15
LengthKB      : 0,00

Mode          : d-r--
Name          : Menú Inicio
Length        :
LastWriteTime : 12/11/2009 16:19:32
LengthKB      : 0,00

Mode          : d-r--
Name          : Mis documentos
Length        :
LastWriteTime : 21/04/2010 11:20:31
LengthKB      : 0,00

Mode          : d---s
Name          : UserData
Length        :
LastWriteTime : 19/11/2009 23:32:02
LengthKB      : 0,00

Mode          : -a---
Name          : a.txt
Length        : 46
LastWriteTime : 16/04/2010 11:44:37
LengthKB      : 0,04

Mode          : -a---
Name          : b.txt
Length        : 44
LastWriteTime : 16/04/2010 11:47:54
LengthKB      : 0,04

¡Vaya, eso debería ponernos la mosca detrás de la oreja! Si no hemos especificado formato, debería darnos el formato de tabla, pues según Get-Member se sigue tratando de directorios y ficheros. Esto no es cierto. Todo objeto en PowerShell tiene un método GetType que nos devuelve el tipo de objeto. Vamos a ver qué devuelve realmente Get-ChildItem y Get-MyChildItem:

PS C:\> Get-ChildItem | ft Name,@{Name="Tipo";Expression={$_.GetType()};Alignment="Left"}

Name                                                        Tipo
----                                                        ----
Cookies                                                     System.IO.DirectoryInfo
Escritorio                                                  System.IO.DirectoryInfo
Favoritos                                                   System.IO.DirectoryInfo
Menú Inicio                                                 System.IO.DirectoryInfo
Mis documentos                                              System.IO.DirectoryInfo
UserData                                                    System.IO.DirectoryInfo
a.txt                                                       System.IO.FileInfo
b.txt                                                       System.IO.FileInfo


PS C:\> Get-MyChildItem | ft Name,@{Name="Tipo";Expression={$_.GetType()};Alignment="Left"}

Directorio C:\Documents and Settings\Lab

Name                                                        Tipo
----                                                        ----
Cookies                                                     System.Management.Automation.PSCustomObject
Escritorio                                                  System.Management.Automation.PSCustomObject
Favoritos                                                   System.Management.Automation.PSCustomObject
Menú Inicio                                                 System.Management.Automation.PSCustomObject
Mis documentos                                              System.Management.Automation.PSCustomObject
UserData                                                    System.Management.Automation.PSCustomObject
a.txt                                                       System.Management.Automation.PSCustomObject
b.txt                                                       System.Management.Automation.PSCustomObject

¡Ahora queda claro! Pese a lo que diga Get-Member, lo que realmente está devolviendo nuestra función son objetos PSCustomObject, cuyo formato predeterminado es el de lista.

PS C:\> Get-ChildItem | ft Name,@{Name="Tipo";Expression={$_.GetType()};Alignment="Left"}

Name                                                        Tipo
----                                                        ----
Cookies                                                     System.IO.DirectoryInfo
Escritorio                                                  System.IO.DirectoryInfo
Favoritos                                                   System.IO.DirectoryInfo
Menú Inicio                                                 System.IO.DirectoryInfo
Mis documentos                                              System.IO.DirectoryInfo
UserData                                                    System.IO.DirectoryInfo
a.txt                                                       System.IO.FileInfo
b.txt                                                       System.IO.FileInfo

Es decir, hemos perdido propiedades y además hemos perdido el formato predeterminado ¡Vaya, sí que la hemos hecho buena! ¡Ahora no puedo usar ese objeto para filtrar u ordenar según, por ejemplo, la fecha de creación y además me muestra el resutado en una incómoda lista, en lugar de hacerlo en una tabla! Además no me muestra la tabla de ficheros, si no una lista bastante incómoda de leer ¡¡Vaya tostón, eso no me gusta!! No hay problema, para resolver esto Add-Member viene al rescate.

Add-Member

Add-Member permite agregar a un objeto aquellos miembros (propiedades o métodos) que deseemos a un objeto ya existente, con lo que el problema que vimos en el punto anterior de perder propiedades y métodos, o de tener que enumerar todos en Select-Object y que además perdamos el formato predeterminado del tipo de objeto original, queda solventado. Modifiquemos la función anterior así:

Function Get-MyChildItem($Ruta="$((PWD).Path)")
{
    Write-Host "`n`rDirectorio $Ruta" 
    $Listado = Get-ChildItem -Path $Ruta
    ForEach($Elemento in $Listado)
    {
        $Elemento | Add-Member -MemberType NoteProperty `
                               -Name LengthKB `
                               -Value "$(""{0:N2}"" -f ($Elemento.Length / 1kb))"
        $Elemento
    }
}

Si ejecutamos esta función y miramos con Get-Member qué compone su devolución, observamos que está formada por carpetas y ficheros, con todas sus propiedades y métodos y además se ha agregado la propiedad LengthKB a los ficheros:

PS C:\> Get-MyChildItem | Get-Member

Directorio C:\


   TypeName: System.IO.DirectoryInfo

Name                      MemberType     Definition
----                      ----------     ----------
Mode                      CodeProperty   System.String Mode{get=Mode;}
Create                    Method         System.Void Create(System.Security.AccessControl.DirectorySecurity director...
CreateObjRef              Method         System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
CreateSubdirectory        Method         System.IO.DirectoryInfo CreateSubdirectory(string path), System.IO.Director...
Delete                    Method         System.Void Delete(), System.Void Delete(bool recursive)
Equals                    Method         bool Equals(System.Object obj)
GetAccessControl          Method         System.Security.AccessControl.DirectorySecurity GetAccessControl(), System....
GetDirectories            Method         System.IO.DirectoryInfo[] GetDirectories(string searchPattern), System.IO.D...
GetFiles                  Method         System.IO.FileInfo[] GetFiles(string searchPattern), System.IO.FileInfo[] G...
GetFileSystemInfos        Method         System.IO.FileSystemInfo[] GetFileSystemInfos(string searchPattern), System...
GetHashCode               Method         int GetHashCode()
GetLifetimeService        Method         System.Object GetLifetimeService()
GetObjectData             Method         System.Void GetObjectData(System.Runtime.Serialization.SerializationInfo in...
GetType                   Method         type GetType()
InitializeLifetimeService Method         System.Object InitializeLifetimeService()
MoveTo                    Method         System.Void MoveTo(string destDirName)
Refresh                   Method         System.Void Refresh()
SetAccessControl          Method         System.Void SetAccessControl(System.Security.AccessControl.DirectorySecurit...
ToString                  Method         string ToString()
LengthKB                  NoteProperty   System.String LengthKB=0,00
PSChildName               NoteProperty   System.String PSChildName=aeee88676f12056b7a7bcabaf65d83
PSDrive                   NoteProperty   System.Management.Automation.PSDriveInfo PSDrive=C
PSIsContainer             NoteProperty   System.Boolean PSIsContainer=True
PSParentPath              NoteProperty   System.String PSParentPath=Microsoft.PowerShell.Core\FileSystem::C:\
PSPath                    NoteProperty   System.String PSPath=Microsoft.PowerShell.Core\FileSystem::C:\aeee88676f120...
PSProvider                NoteProperty   System.Management.Automation.ProviderInfo PSProvider=Microsoft.PowerShell.C...
Attributes                Property       System.IO.FileAttributes Attributes {get;set;}
CreationTime              Property       System.DateTime CreationTime {get;set;}
CreationTimeUtc           Property       System.DateTime CreationTimeUtc {get;set;}
Exists                    Property       System.Boolean Exists {get;}
Extension                 Property       System.String Extension {get;}
FullName                  Property       System.String FullName {get;}
LastAccessTime            Property       System.DateTime LastAccessTime {get;set;}
LastAccessTimeUtc         Property       System.DateTime LastAccessTimeUtc {get;set;}
LastWriteTime             Property       System.DateTime LastWriteTime {get;set;}
LastWriteTimeUtc          Property       System.DateTime LastWriteTimeUtc {get;set;}
Name                      Property       System.String Name {get;}
Parent                    Property       System.IO.DirectoryInfo Parent {get;}
Root                      Property       System.IO.DirectoryInfo Root {get;}
BaseName                  ScriptProperty System.Object BaseName {get=$this.Name;}


   TypeName: System.IO.FileInfo

Name                      MemberType     Definition
----                      ----------     ----------
Mode                      CodeProperty   System.String Mode{get=Mode;}
AppendText                Method         System.IO.StreamWriter AppendText()
CopyTo                    Method         System.IO.FileInfo CopyTo(string destFileName), System.IO.FileInfo CopyTo(s...
Create                    Method         System.IO.FileStream Create()
CreateObjRef              Method         System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
CreateText                Method         System.IO.StreamWriter CreateText()
Decrypt                   Method         System.Void Decrypt()
Delete                    Method         System.Void Delete()
Encrypt                   Method         System.Void Encrypt()
Equals                    Method         bool Equals(System.Object obj)
GetAccessControl          Method         System.Security.AccessControl.FileSecurity GetAccessControl(), System.Secur...
GetHashCode               Method         int GetHashCode()
GetLifetimeService        Method         System.Object GetLifetimeService()
GetObjectData             Method         System.Void GetObjectData(System.Runtime.Serialization.SerializationInfo in...
GetType                   Method         type GetType()
InitializeLifetimeService Method         System.Object InitializeLifetimeService()
MoveTo                    Method         System.Void MoveTo(string destFileName)
Open                      Method         System.IO.FileStream Open(System.IO.FileMode mode), System.IO.FileStream Op...
OpenRead                  Method         System.IO.FileStream OpenRead()
OpenText                  Method         System.IO.StreamReader OpenText()
OpenWrite                 Method         System.IO.FileStream OpenWrite()
Refresh                   Method         System.Void Refresh()
Replace                   Method         System.IO.FileInfo Replace(string destinationFileName, string destinationBa...
SetAccessControl          Method         System.Void SetAccessControl(System.Security.AccessControl.FileSecurity fil...
ToString                  Method         string ToString()
LengthKB                  NoteProperty   System.String LengthKB=0,00
PSChildName               NoteProperty   System.String PSChildName=a.txt
PSDrive                   NoteProperty   System.Management.Automation.PSDriveInfo PSDrive=C
PSIsContainer             NoteProperty   System.Boolean PSIsContainer=False
PSParentPath              NoteProperty   System.String PSParentPath=Microsoft.PowerShell.Core\FileSystem::C:\
PSPath                    NoteProperty   System.String PSPath=Microsoft.PowerShell.Core\FileSystem::C:\a.txt
PSProvider                NoteProperty   System.Management.Automation.ProviderInfo PSProvider=Microsoft.PowerShell.C...
Attributes                Property       System.IO.FileAttributes Attributes {get;set;}
CreationTime              Property       System.DateTime CreationTime {get;set;}
CreationTimeUtc           Property       System.DateTime CreationTimeUtc {get;set;}
Directory                 Property       System.IO.DirectoryInfo Directory {get;}
DirectoryName             Property       System.String DirectoryName {get;}
Exists                    Property       System.Boolean Exists {get;}
Extension                 Property       System.String Extension {get;}
FullName                  Property       System.String FullName {get;}
IsReadOnly                Property       System.Boolean IsReadOnly {get;set;}
LastAccessTime            Property       System.DateTime LastAccessTime {get;set;}
LastAccessTimeUtc         Property       System.DateTime LastAccessTimeUtc {get;set;}
LastWriteTime             Property       System.DateTime LastWriteTime {get;set;}
LastWriteTimeUtc          Property       System.DateTime LastWriteTimeUtc {get;set;}
Length                    Property       System.Int64 Length {get;}
Name                      Property       System.String Name {get;}
BaseName                  ScriptProperty System.Object BaseName {get=if ($this.Extension.Length -gt 0){$this.Name.Re...
VersionInfo               ScriptProperty System.Object VersionInfo {get=[System.Diagnostics.FileVersionInfo]::GetVer...

De todas formas, hemos aprendido a desconfiar de Get-Member ¿Verdad? Veamos qué devuelve GetType:

PS C:\> Get-MyChildItem | ft Name,@{Name="Tipo";Expression={$_.GetType()};Alignment="Left"}

Directorio C:\Documents and Settings\Lab

Name                                                        Tipo
----                                                        ----
Cookies                                                     System.IO.DirectoryInfo
Escritorio                                                  System.IO.DirectoryInfo
Favoritos                                                   System.IO.DirectoryInfo
Menú Inicio                                                 System.IO.DirectoryInfo
Mis documentos                                              System.IO.DirectoryInfo
UserData                                                    System.IO.DirectoryInfo
a.txt                                                       System.IO.FileInfo
b.txt                                                       System.IO.FileInfo 

¡Bien, esto era lo que buscamos, ahora sí podemos pasar, por ejemplo, la salida de nuestra función a Sort-Object como hicimos anteriormente con Get-ChildItem:

PS C:\> Get-MyChildItem | Sort-Object CreationTime `
                                      | Format-Table Mode,CreationTime,Length,Name -Autosize

Directorio C:\Documents and Settings\Lab

Mode  CreationTime        Length Name
----  ------------        ------ ----
d-r-- 12/11/2009 16:51:29        Mis documentos
d-r-- 12/11/2009 16:51:29        Menú Inicio
d---- 12/11/2009 16:51:29        Escritorio
d-r-- 12/11/2009 16:51:29        Favoritos
d---s 12/11/2009 16:51:29        Cookies
d---s 19/11/2009 23:32:02        UserData
-a--- 16/04/2010 11:44:10 46     a.txt
-a--- 16/04/2010 11:47:54 44     b.txt

¡Esto es justo lo que buscábamos ¿verdad? Hemos conseguido conservar el formato y el tipo. Definitivamente, es mejor usar Add-Member que Select-Object, al menos en este caso.

Vamos a ver otra habilidad de Add-Member que permite abreviar el código: el modificador PassThru. Este modificador de Add-Member permite que sea devuelto el objeto al que se agrega el miembro, lo que nos permite reescribir la función de esta manera, en la cual suprimimos la línea de devolución, pues va incluída con Add-Member:

Function Get-MyChildItem($Ruta="$((PWD).Path)")
{
    Write-Host "`n`rDirectorio $Ruta" 
    $Listado = Get-ChildItem -Path $Ruta
    ForEach($Elemento in $Listado)
    {
        Add-Member -InputObject $Elemento -MemberType NoteProperty `
                                -PassThru `
                                -Name LengthKB `
                                -Value "$(""{0:N2}"" -f ($Elemento.Length / 1kb))"
    }
}

Add-Member permite agregar los siguientes tipos de miembros:

No todos los objetos tienen todos los tipos de miembros. Si se especifica un tipo de miembro que el objeto no tiene se producirá un error.

Los miembros de tipo Event no son válidos para Add-Member.

En el día a día, los tipos que se agregan son AliasProperty, NoteProperty y ScriptMethod. El uso de AliasProperty está indicado cuando un objeto tiene una propiedad que sería válida para ser usada en el encaminamiento a otro Cmdlet, pero su nombre no coincide con el esperado por ese Cmdlet. En este caso el crear un alias que devuelva el contenido de la propiedad original, y se llame como la propiedad que mira el Cmdlet de destino en el objeto que recibe por encaminamiento, nos permite encaminar la salida a ese Cmdlet, cosa que no se podría hacer con el objeto original; supongamos que el objeto encaminado tiene una propiedad ServerName y el Cmdlet de destino espera una propiedad ComputerName, si creamos el alias ComputerName con el contenido de ServerName, el objeto podrá ser encaminado.

En el ejemplo anterior hemos creado un miembro de tipo NoteProperty. Si quisieramos saber, por ejemplo, el propietario del fichero, podríamos agregar un método de script a la devolución para obtenerlo:

Function Get-MyChildItem($Ruta="$((PWD).Path)")
{
    Write-Host "`n`rDirectorio $Ruta" 
    $Listado = Get-ChildItem -Path $Ruta
    ForEach($Elemento in $Listado)
    {
        $Elemento | Add-Member -MemberType NoteProperty `
                               -Name LengthKB `
                               -Value "$(""{0:N2}"" -f ($Elemento.Length / 1kb))"
        Add-Member -InputObject $Elemento -MemberType ScriptMethod `
                                -Name Owner `
                                -Value {(Get-Acl $this.FullName).Owner} `
                                -PassThru
    }
}

En el primer Add-Member encaminamos el propio objeto a Add-Member y le agregamos la propiedad LengthKB; de esta manera Add-Member no devuelve nada, y por tanto la función de momento no ha devuelto nada. En el segundo Add-Member, sin embargo, lo que hacemos es ejecutar el Cmdlet pasandole el objeto como parámetro y agregamos el modificador PassThru, con lo que se produce la devolución de la función. El código del método que hemos agregado se debe encerrar entre llaves. Para obtener un listado que muestre el propietario, podríamos hacer algo así:

PS C:\> Get-MyChildItem | `
            ft Name,@{Name="Propietario";Expression={$_.Owner()};Alignment="Left"}

Directorio C:\Documents and Settings\Lab

Name                                                        Propietario
----                                                        -----------
Cookies                                                     LABXP\Lab
Escritorio                                                  LABXP\Lab
Favoritos                                                   LABXP\Lab
Menú Inicio                                                 LABXP\Lab
Mis documentos                                              LABXP\Lab
UserData                                                    LABXP\Lab
a.txt                                                       LABXP\Lab
b.txt                                                       LABXP\Lab

Por supuesto, este ha sido un ejemplo forzado, pues en este caso concreto habría sido más cómodo usar también una NoteProperty:

Function Get-MyChildItem($Ruta="$((PWD).Path)")
{
    Write-Host "`n`rDirectorio $Ruta" 
    $Listado = Get-ChildItem -Path $Ruta
    ForEach($Elemento in $Listado)
    {
        $Elemento | Add-Member -MemberType NoteProperty `
                               -Name LengthKB `
                               -Value "$(""{0:N2}"" -f ($Elemento.Length / 1kb))"
        Add-Member -InputObject $Elemento -MemberType NoteProperty `
                                -Name Owner `
                                -Value (Get-Acl $Elemento.FullName).Owner `
                                -PassThru
    }
}

De esta manera podemos invocar a la propiedad directamente desde Format-Table, por ejemplo:

PS C:\> Get-MyChildItem | Format-Table Name,Owner -AutoSize

Directorio C:\

Name                           Owner
----                           -----
aeee88676f12056b7a7bcabaf65d83 LABXP\Lab
Archivos de programa           BUILTIN\Administradores
Documents and Settings         BUILTIN\Administradores
Pruebas                        LABXP\Lab
WINDOWS                        BUILTIN\Administradores
a.txt                          LABXP\Lab
AUTOEXEC.BAT                   BUILTIN\Administradores
b.txt                          LABXP\Lab
bat.bat                        LABXP\Lab
c.txt                          LABXP\Lab
CONFIG.SYS                     BUILTIN\Administradores
msinfo.txt                     LABXP\Lab
oab.bat                        LABXP\Lab
pepe.txt                       LABXP\Lab
software.tab                   LABXP\Lab
software.tsv                   LABXP\Lab
usuarios.tab                   LABXP\Lab

No siempre nos bastará con agregar propiedades o métodos a un objeto, si no que querremos crear un objeto desde cero. Es lo que veremos a continuación.

Creación de nuevos tipos de objeto

Para la creación de nuevos tipos de objetos nos sirven Select-Object, Add-Member y Add-Type, los tres en conjunción con New-Object.

New-Object, Select-Object, Add-Member y tipos de objetos

New-Object permite la creación de un nuevo objeto, pudiendo especificar el tipo de objeto que queremos que se cree. Cuando queremos crear un objeto en blanco para luego ir añadiendo los métodos y propiedades que necesitemos, lo mejor es crear un objeto de tipo Object o PSCustomObject:

PS C:\> $a = New-Object -TypeName Object
PS C:\> $b = New-Object -TypeName PSCustomObject
PS C:\> $a | Get-Member


   TypeName: System.Object

Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()


PS C:\> $b | Get-Member


   TypeName: System.Management.Automation.PSCustomObject

Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()


PS C:\> $a.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object


PS C:\> $b.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    PSCustomObject                           System.Object

Como podemos observar, ambos tipos devuelven un objeto sin propiedades y con cuatro métodos intrínsecos a todo objeto .NET. Al ejecutar el método GetType obtenemos el tipo de objeto y la clase de la que hereda, que como podemos ver Object no hereda de ninguna y PSCustomObject hereda de Object.

Si usamos Select-Object con este objeto para agregarle propiedades, vemos cómo cambia su tipo a PSCustomObject:

PS C:\> $a = $a | Select-Object Nombre,Apellido1,Apellido2,Edad,Sexo
PS C:\> $a.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    PSCustomObject                           System.Object

No obstante, Get-Member seguirá evaluándolo como el tipo original:

PS C:\> $a | Get-Member


   TypeName: Selected.System.Object

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
Apellido1   NoteProperty  Apellido1=null
Apellido2   NoteProperty  Apellido2=null
Edad        NoteProperty  Edad=null
Nombre      NoteProperty  Nombre=null
Sexo        NoteProperty  Sexo=null

Esto no sucede si usamos Add-Member, el tipo devuelto es el original tanto por Get-Member como por GetType:.

PS C:\> $a = New-Object -TypeName Object
PS C:\> $a | Get-Member


   TypeName: System.Object

Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()


PS C:\> $a.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object


PS C:\> $a | Add-Member -MemberType NoteProperty -Name Nombre -Value $null
PS C:\> $a | Add-Member -MemberType NoteProperty -Name Apellido1 -Value $null
PS C:\> $a | Add-Member -MemberType NoteProperty -Name Apellido2 -Value $null
PS C:\> $a | Add-Member -MemberType NoteProperty -Name Edad -Value $null
PS C:\> $a | Add-Member -MemberType NoteProperty -Name Sexo -Value $null
PS C:\> $a.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object


PS C:\> $a | Get-Member


   TypeName: System.Object

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
Apellido1   NoteProperty  Apellido1=null
Apellido2   NoteProperty  Apellido2=null
Edad        NoteProperty  Edad=null
Nombre      NoteProperty  Nombre=null
Sexo        NoteProperty  Sexo=null

Esto ya lo vimos antes, Add-Member respeta los tipos y añade propiedades y/o métodos, mientras que Select-Object siempre convierte los objetos en PSCustomObject. Ahora, que tanto en un caso como en otro las propiedades son NoteProperty y los métodos ScriptMethod, y al pasar por Get-Member nos devuelven su nombre y su contenido, cosa que no hacen los tipos "de verdad".

Add-Type

Si queremos crear un tipo personalizado "de verdad" podemos definirlo con C# y pasarselo a Add-Type. Muy importante, la definición del tipo debe comenzar por arroba seguido de dobles comillas y salto de línea y debe acabar con salto de línea dobles comillas y arroba (es decir, que es de tipo cadena con múltiples líneas):

PS C:\> $Persona = @"
>> namespace GualtrySoft
>> {
>>     public class Persona
>>     {
>>         private string _Nombre = "";
>>         private string _Apellido1 = "";
>>         private string _Apellido2 = "";
>>         private int _Edad = 0;
>>         private string _Sexo = "Poco";
>>         public string Nombre
>>         {
>>             get
>>             {
>>                 return _Nombre;
>>             }
>>             set
>>             {
>>                 _Nombre = value;
>>             }
>>         }
>>         public string Apellido1
>>         {
>>             get
>>             {
>>                 return _Apellido1;
>>             }
>>             set
>>             {
>>                 _Apellido1 = value;
>>             }
>>         }
>>         public string Apellido2
>>         {
>>             get
>>             {
>>                 return _Apellido2;
>>             }
>>             set
>>             {
>>                 _Apellido2 = value;
>>             }
>>         }
>>         public int Edad
>>         {
>>             get
>>             {
>>                 return _Edad;
>>             }
>>             set
>>             {
>>                 _Edad = value;
>>             }
>>         }
>>         public string Sexo
>>         {
>>             get
>>             {
>>                 return _Sexo;
>>             }
>>             set
>>             {
>>                 _Sexo = value;
>>             }
>>         }
>>     }
>> }
>> "@
>>

Hemos almacenado en la variable $Persona la definición en C# de una clase que está ubicada en el espacio de nombres GualtrySoft. Una vez tenemos la definición, podremos definir esta clase usando Add-Type y pasando la variable $Persona como parámetro TypeDefinition:

PS C:\> Add-Type -TypeDefinition $Persona

Una vez definida la clase, podemos crear un objeto de este tipo con New-Object:

PS C:\> $Faustino = New-Object GualtrySoft.Persona

Podemos ahora pasar a darle valor a sus propiedades:

PS C:\> $Faustino.Nombre = "Faustino"
PS C:\> $Faustino.Apellido1 = "Mejo"
PS C:\> $Faustino.Apellido2 = "Dasrrodas"
PS C:\> $Faustino.Edad = 23
PS C:\> $Faustino.Sexo = "Más quisiera"
PS C:\> $Faustino


Nombre    : Faustino
Apellido1 : Mejo
Apellido2 : Dasrrodas
Edad      : 23
Sexo      : Más quisiera

Si obtenemos sus propiedades con Get-Member vemos que esta vez son propiedades "de verdad", no NoteProperties y además en la definición no se ve el valor que tiene en la instancia pasada a Get-Member, como sí pasaba con las NoteProperties, si no que observamos que tiene los métodos Get y Set. Si obtenemos su tipo con GetType vemos que es de tipo Persona, no PSCustomObject:

PS C:\> $Faustino | Get-Member


   TypeName: GualtrySoft.Persona

Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()
Apellido1   Property   System.String Apellido1 {get;set;}
Apellido2   Property   System.String Apellido2 {get;set;}
Edad        Property   System.Int32 Edad {get;set;}
Nombre      Property   System.String Nombre {get;set;}
Sexo        Property   System.String Sexo {get;set;}


PS C:\> $Faustino.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    Persona                                  System.Object
Función para la creación de definiciones de tipo para ser usadas con Add-Type

Para hacer un poco más rápida la definicion de un tipo con código C#, he ideado esta función, que devuelve la definición, de manera que sea utilizada por Add-Type. Gracias a la nueva caracerística de PowerShell 2.0 de ayuda basada en comentarios, contiene la ayuda de su forma de uso; ejecutada una vez se puede invocar para crear una definición de tipo o para obtener la ayuda con Get-Help New-TypeDefinition, tal y como se haría con otro Cmdlet. Por supuesto no genera código que "haga algo", sólo la definición básica del objeto, lo que incluye:

En determinados casos esta declaración básica (sobre todo si no hay declaración de métodos) será suficiente para nuestros propósitos, pues nos dotará de un objeto personalizado al que daremos valor a sus propiedades. En casos más complejos, podemos usar esta función para obtener una plantilla sobre la que desarrollar el código que necesitemos.

Este es el código de la función:

Function New-TypeDefinition
(
    [parameter(Mandatory=$true,
               Position=0,
               HelpMessage="Falta el nombre de la definición de clase a crear")]
    [Alias("N","Clase")]
    [string]
    $Nombre,
    [parameter(Mandatory=$false,
               Position=1)]
    [Alias("LE","PropLE","LecturaEscritura")]
    [string]
    $PropiedadesLecturaEscritura = "",
    [parameter(Mandatory=$false,
               Position=2)]
    [Alias("SL","PropSL","SoloLectura")]
    [string]
    $PropiedadesSoloLectura = "",
    [parameter(Mandatory=$false,
               Position=3)]
    [Alias("M","Met","MD")]
    [string]
    $Metodos = "",
    [parameter(Mandatory=$false,
               Position=4)]
    [Alias("B","ClaseBase")]
    [string]
    $Base = "",
    [parameter(Mandatory=$false,
               Position=5)]
    [Alias("U","US","I","IM","IMP","Imports")]
    [string]
    $Usings = "",
    [parameter(Mandatory=$false,
               Position=6)]
    [Alias("T","Sangria","S","Sang","TB")]
    [byte]
    $Tab = 4
)
{
#.Synopsis
#    Esta función crea la declaración en C# de una clase.
#.Description
#    Esta función crea una definición de clase en C# y la devuelve preparada para
#    ser utilizada como parámetro TypeDefinition del Cmdlet Add-Type. La función
#    es capaz de crear la clase en el espacio de nombres que se desee y de crear
#    propiedades, tanto de lectura escritura como de sólo lectura. Crea así mismo 
#    métodos, incluso con sobrecarga, todo depende de cómo se pasen los
#    parámetros.
#    Si necesitamos un objeto con propiedades en las que guardar los datos que
#    necesitemos (sin ningún tipo de comprobación ni proceso a parte del propio
#    de establecer o leer el contenido de las propiedades que tenga definidas),
#    para ser usado como devolución de un Cmdlet que desarrollemos, se puede
#    pasar una llamada a esta función directamente como parámetro TypeDefinition
#    del Cmdlet Add-Type.
#    En el caso de que la clase sea más compleja, tendremos que escribir su
#    código, y este Cmdlet puede ser útil como plantilla desde la que comenzar su
#    desarrollo.
#.Parameter Nombre
#    Nombre de la clase a crear. Si se pasa un nombre con puntos, el punto se
#    considera separador de un array en el cual el primer elemento por la derecha
#    es el nombre de la clase y los demás son espacios de nombres anidados. Por
#    ejemplo, si se pasa "TIA.Bacterio.ControladorFibroCombustional" se creará
#    la clase "ControladorFibroCombustional" dentro del espacio de nombres
#    "Bacterio", que a su vez estará contenido dentro del espacio de nombres
#    "TIA".
#   Este parámetro tiene como alias N y Clase
#.Parameter PropiedadesLecturaEscritura
#    Cadena en la que se definen las propiedades de escritura y lectura que
#    tendrá la clase, separadas por comas. Cada elemento del array estará formado
#    de cuatro campos separados por punto y coma;  estos campos son:
#        ambito;tipo;nombre;valor predeterminado
#    Siendo cada uno de estos:
#        -ambito tipo: public o private, y modificadores (override, static, void,
#           etc)
#       -tipo: tipo de dato (int, bool,etc.). Por ejemplo "int". 
#        -nombre: nombre de la propiedad.
#        -valor predeterminado: valor predeterminado, si es que tiene alguno. En
#            el caso de propiedades de tipo string, el valor deberá estar
#            encerrado entre comillas, lo que implica ponerlas dobles o quitarles
#            el significado con acento grave.
#            Ejemplos:
#                "public;int;NDisparos;100,public string;Fabricante;""TIA"""
#                "public;int;NDisparos;100,public string;Fabricante;`"TIA`""
#            Ambos ejemplos definen dos propiedades NDisparos, pública, entera,
#            con valor predeterminado 100, y Fabricante, pública, de cadena y con
#            valor pretederminado TIA.
#   Este parámetro tiene como alias LE, PropLE y LecturaEscritura
#.Parameter PropiedadesSoloLectura
#    Propiedades de solo lectura. La nomenclatura es exactamente la misma que la
#    de las propiedades de lectura escritura.
#   Este parámetro tiene como alias SL, PropSL y SoloLectura
#.Parameter Metodos
#    Cadena en la que se definen los métodos que tendrá la clase, separados por
#    comas. Cada elemento es tal y como querríamos que se declarara en C#, por
#    tanto, su nomenclatura es:
#
#        "ambito [herencia] tipo nombre1(tipo1 argumento1,...,tipoN argumentoN)"
#
#    Siendo:
#    
#        -ambito (requerido): public o private.
#        -herencia (opcional): orientado a polimorfismo, puede ser virtual u
#            override.
#        -nombre (requerido): nombre del método.
#        -argumentos (opcionales): argumentos que recibe el método, cada uno de
#            ellos sigue la nomenclatura de argumentos de C# (tipo nombre)
#        
#    En el caso de métodos sin devolución, pasamos como tipo void, en el caso de
#    funciones, pasamos como tipo el de la devolución que tendrá. Por ejemplo:
#    "public void Disparar,private int NDisparos" definirá dos métodos, uno
#    público sin devolución ("Disparar") y otro privado que devuelve enteros
#    ("NDisparos"). En el caso de que queramos sobrecarga, se debe definir el 
#    método tantas veces como formas de llamada queramos que tenga; por ejemplo
#    "public int TDisparo(int e_TDisparo),public int TDisparo(string s_TDisparo)"
#    Si lo que estamos definiendo es una clase que usaremos de base para otras
#    que sean polimórficas de ella, podemos usar virtual:
#        "public virtual void Disparar"
#    De igual manera, si lo que estamos es definiendo una clase derivada podemos
#    user override:
#        "public override void Disparar"
#    Ejemplo de este parámetro:
#        "public void Disparar,public int TDisparo(int e_TDisparo)" 
#   Este parámetro tiene como alias M, Met y MD
#.Parameter Usings
#    Define las entradas using de la clase. Se trata de un parámetro de tipo
#    cadena con los valores de los espacios de nombre a usar separados por comas. 
#    Por ejemplo: "System,vb = Microsoft.VisualBasic" provocará que se usen los
#    espacios de nombres System y Microsoft.VisualBasic, este último ademas con
#    el alias vb.
#   Este parámetro tiene como alias U, US, I, IM, IMP e Imports
#.Parameter Base
#    Nombre de la clase en la que se está basada la que estamos definiendo; por
#    ejemplo "TIA.Bacterio.Armas.Arma".
#   Este parámetro tiene como alias B y ClaseBase
#.Parameter Tab
##    Número de espacios que se sangra el código C# generado. De manera
#    predeterminada el sangrado será a 4 espacios.
#   Este parámetro tiene como alias T, Sangria, S, Sang y TB
#.Example
#        Create-TypeDefinition "TIA.Bacterio.Arma.ControladorFibroCombustional"
#    En este caso se crea una clase vacía, sin propiedades ni métodos, ni using
#    ni estando basada en ninguna otra clase. Se creará en el espacio de nombres
#    TIA.Bacterio.Armas.  Esta sería la devolución de esta llamada:
#    
#    namespace TIA
#    {
#        namespace Bacterio
#        {
#            namespace Armas
#            {
#                public class ControladorFibroCombustional
#                {
#                }
#            }
#        }
#    }
#.Example
#        $SL = "public;string;Version;`"1.0`"0,public;string;Fabricante;""TIA"""
#        $LE = "public;int;NDisparos;0,public;bool;ArmaUtil;false"
#        $Me = "public override void Disparo,public override void Rafaga"
#        $Base = "cls_Arma"
#        $Usings = "System,TIA.Bacterio.Armas"
#        New-TypeDefinition "TIA.Bacterio.Armas.DesfibriladorNeuronal" `
#                            -PropiedadesSoloLectura $SL `
#                            -PropiedadesLecturaEscritura $LE `
#                            -Metodos $Me
#                            -Base $Base -Usings $Usings -Tab 2
#   En este caso se crea una definición de una clase que contiene dos
#   propiedades de solo lectura (Version y Fabricante), dos de lectura/escritura
#   (NDisparos y ArmaUtil), dos métodos (Disparo y Rafaga), está basada en la
#   clase Arma y utiliza los espacios de nombres System y TIA.Bacterio.Armas
#    La devolucion en este caso será
#    
#    namespace TIA
#    {
#      namespace Bacterio
#      {
#        namespace Armas
#        {
#          public class DesfibriladorNeuronal : Arma
#          {
#            private int _NDisparos = 0;
#            private bool _ArmaUtil = false;
#            private string _Version = "1.00";
#            private string _Fabricante = "TIA";
#            public int NDisparos
#            {
#              get
#              {
#                return _NDisparos;
#              }
#              set
#              {
#                _NDisparos = value;
#              }
#            }
#            public bool ArmaUtil
#            {
#              get
#              {
#                return _ArmaUtil;
#              }
#              set
#              {
#                _ArmaUtil = value;
#              }
#            }
#            public string Version
#            {
#              get
#              {
#                return _Version;
#              }
#            }
#            public string Fabricante
#            {
#              get
#              {
#                return _Fabricante;
#              }
#            }
#            public override void Disparo()
#            {
#    
#            }
#            public override void Rafaga()
#            {
#    
#            }
#          }
#        }
#      }
#    }

#
#
#
    $arr_Nombre = $Nombre.Split(".")
    # En la línea anterior, obtenemos un array con los elementos del nombre de
    # la clase, usando el punto (.) como separador.
    
    # Recorremos los elementos del array obtenido. Usamos For en lugar de 
    # ForEach para identificar con comodidad el último elemento. Esto es
    # necesario ya que este último elemento es el nombre de la clase, y el resto
    # son nombres de namespaces
    For($int_ID=0;$int_ID -lt $arr_Nombre.Count;$int_ID++)
    {
        # Para sangrar el texto, indentaremos según el número de elemento. Para
        # ello multiplicamos el número de espacios de sangrado que se recibe
        # como parámetro por el índice actual
        $int_Tab = $int_ID * $Tab
        
        # Si no es el último elemento construiremos una declaración de namespace
        If($int_ID -lt ($arr_Nombre.Count - 1))
        {
            $Definicion = $Definicion + `
                          (" " * $int_Tab) + "namespace " + $arr_Nombre[$int_ID] + `
                          "`n`r" + (" " * $int_Tab) + "{`n`r"
        }
        # Si es el último elemento construiremos una declaración de clase
        Else
        {
            $Definicion = $Definicion + `
                          (" " * $int_Tab) + "public class " + $arr_Nombre[$int_ID]
            # Si es derivada de otra clase lo ponemos
            If($Base -ne "" -and $Base -ne $null)
            {
                $Definicion = $Definicion + " : $Base"
            }
            $Definicion = $Definicion + `
                        "`n`r" + `
                        (" " * $int_Tab) + "{`n`r"
        }
    }
    
    # Vamos a empezar la construcción de la definición de la clase.
    # Establecemos el sangrado al nivel de la definición de la clase, es decir
    # el índice del nombre de clase multiplicado por los espacio de sangrado
    $int_Tab = $int_Tab + $Tab
    
    If($PropiedadesLecturaEscritura -ne "")
    {
        # Recorremos los miembros del array de propiedades de lectura escritura
        ForEach($Miembro In $PropiedadesLecturaEscritura.Split(","))
        {
            # En la variable declaración vamos construyendo la declarión de las
            # variables internas de las propiedades, poniendo un guion bajo delante
            # del nombre de la propiedad, poniendo su tipo como el valor
            # correspondiente a la clave del hashtable y declarándola como privada
            $Ambito = $Miembro.Split(";")[0]
            $Tipo = $Miembro.Split(";")[1]
            $NombrePropiedad = $Miembro.Split(";")[2]
            $ValorPredeterminado = $Miembro.Split(";")[3]
            
            $Declaracion = $Declaracion + `
                        (" " * $int_Tab) + `
                        "private $Tipo _$NombrePropiedad"
            If ($ValorPredeterminado -ne "")
            {
                $Declaracion = $Declaracion + " = $ValorPredeterminado"
            }
            $Declaracion = $Declaracion + ";`n`r"
            
            # En la variable propiedades vamos poniendo las declaraciones de las
            # funciones get y set de la propiedad
            $Propiedades = $Propiedades + `
        (" " * $int_Tab) + "$Ambito $Tipo $NombrePropiedad`n`r" + `
        (" " * $int_Tab) + "{`n`r" + `
        (" " * ($int_Tab + $Tab)) + "get`n`r" + `
        (" " * ($int_Tab + $Tab)) + "{`n`r" + `
        (" " * ($int_Tab + ($Tab * 2))) + "return _$NombrePropiedad;`n`r" + `
        (" " * ($int_Tab + $Tab)) + "}`n`r" + `
        (" " * ($int_Tab + $Tab)) + "set`n`r" + `
        (" " * ($int_Tab + $Tab)) + "{`n`r" + `
        (" " * ($int_Tab + ($Tab * 2))) + "_$NombrePropiedad = value;`n`r" + `
        (" " * ($int_Tab + $Tab)) + "}`n`r" + `
        (" " * $int_Tab) + "}`n`r"
        }
    }
    If($PropiedadesSoloLectura -ne "")
    {
        # Recorremos los miembros del array de propiedades de sólo lectura
        ForEach($Miembro In $PropiedadesSoloLectura.Split(","))
        {
            # En la variable declaración vamos construyendo la declarión de las
            # variables internas de las propiedades, poniendo un guion bajo delante
            # del nombre de la propiedad, poniendo su tipo como el valor
            # correspondiente a la clave del hashtable y declarándola como privada
            $Ambito = $Miembro.Split(";")[0]
            $Tipo = $Miembro.Split(";")[1]
            $NombrePropiedad = $Miembro.Split(";")[2]
            $ValorPredeterminado = $Miembro.Split(";")[3]
            
            $Declaracion = $Declaracion + `
                        (" " * $int_Tab) + `
                        "private $Tipo _$NombrePropiedad"
            If ($ValorPredeterminado -ne "")
            {
                $Declaracion = $Declaracion + " = $ValorPredeterminado"
            }
            $Declaracion = $Declaracion + ";`n`r"
            
            # En la variable propiedades vamos poniendo las declaraciones de la
            # función get de la propiedad
            $Propiedades = $Propiedades + `
        (" " * $int_Tab) + "$Ambito $Tipo $NombrePropiedad`n`r" + `
        (" " * $int_Tab) + "{`n`r" + `
        (" " * ($int_Tab + $Tab)) + "get`n`r" + `
        (" " * ($int_Tab + $Tab)) + "{`n`r" + `
        (" " * ($int_Tab + ($Tab * 2))) + "return _$NombrePropiedad;`n`r" + `
        (" " * ($int_Tab + $Tab)) + "}`n`r" + `
        (" " * $int_Tab) + "}`n`r"
        }
    }
    If($Metodos -ne "")
    {
        # Recorremos los miembros del array de métodos
        ForEach($Miembro In $Metodos.Split(","))
        {
            
            $Procedimientos = $Procedimientos + `
                        (" " * $int_Tab) + `
                        $Miembro
            If ($Miembro.EndsWith -ne ")")
            {
                $Procedimientos = $Procedimientos + "()"
            }
            $Procedimientos = $Procedimientos + "`n`r"
            
            # En la variable método vamos poniendo las declaraciones de las
            # funciones get y set de la propiedad
            $Procedimientos = $Procedimientos + `
        (" " * $int_Tab) + "{`n`r" + `
         (" " * ($int_Tab + $Tab)) + "`n`r" + `
        (" " * $int_Tab) + "}`n`r"
        }
    }
    # Vamos a montar los usings
    If($Usings -ne "")
    {
        ForEach($Using In $Usings.Split(","))
        {
            $Imports = $Imports + `
                    "using $Using;`n`r"
        }
    }
    # Queda por cerrar con llaves la clase y los namespaces, para lo que
    # recorreremos de mayor a menor los índices del array de nombres, de manera
    # que se irá reduciendo el sangrado
    For($int_ID = ($arr_Nombre.Count - 1);$int_ID -ge 0;$int_ID--)
    {
        # Establecemos el sangrado en función del índice
        $int_Tab = $int_ID * $Tab
        # En la variable cierre vamos almacenando las llavesde cierre en sus
        # correspondientes posiciones de sangrado
        $Cierre = $Cierre + `
                  (" " * $int_Tab) + "}`n`r"
    }
    
    # Montamos la definición de clase.
    $Definicion = $Imports + $Definicion + $Declaracion + `
                  $Propiedades + $Procedimientos + $Cierre
    
    # Procedemos a realizar la devolución de la Función. Es muy importante que
    # se monte como arroba (@) dobles comillas, salto de línea, código C#, 
    # dobles comillas y arroba (@) para que así se pueda pasar una llamada a
    # esta función como parámetro TypeDefinition del Cmdlet Add-Type, ya sea
    # habiendo asignado la devolución a una variable o directamente invocando
    # la función como parámetro TypeDefinition.
@"
$Definicion
"@
}

Para definir el tipo que creamos en el punto anterior (GualtrySoft.Persona), podemos almacenar en una variable la definición e invocar después a Add-Type, pasando como parametro TypeDefinition la variable en la que se ha almacenado la devolución de la función:

PS C:\> $clsPersona = New-TypeDefinition "Gualtrysoft.Persona" `
 -LE "public;string;Nombre;"""",public;string;Apellido1;"""",public;string;Apellido2;"""",public;int;Edad;0,public;string;Sexo;""Poco"""
PS C:\> Add-Type -TypeDefinition $clsPersona

Incluso podemos invocar a la función directamente desde Add-Type, pasando su llamada como parámetro TypeDefinition:

PS C:\> Add-Type -TypeDefinition `
                     (New-TypeDefinition "Gualtrysoft.Persona" `
 -LE "public;string;Nombre;"""",public;string;Apellido1;"""",public;string;Apellido2;"""",public;int;Edad;0,public;string;Sexo;""Poco""")

En ambos casos obtendremos lo mismo, la definición de nuestro tipo. Una vez hecho esto, podemos crear un objeto de ese tipo, de la misma manera que hicimos anteriormente:

PS C:\> $Faustino = New-Object GualtrySoft.Persona
PS C:\> $Faustino.Nombre = "Faustino"
PS C:\> $Faustino.Apellido1 = "Mejo"
PS C:\> $Faustino.Apellido2 = "Dasrrodas"
PS C:\> $Faustino.Edad = 23
PS C:\> $Faustino.Sexo = "Más quisiera"
PS C:\> $Faustino


Nombre    : Faustino
Apellido1 : Mejo
Apellido2 : Dasrrodas
Edad      : 23
Sexo      : Más quisiera
PS C:\> $Faustino | Get-Member


   TypeName: GualtrySoft.Persona

Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()
Apellido1   Property   System.String Apellido1 {get;set;}
Apellido2   Property   System.String Apellido2 {get;set;}
Edad        Property   System.Int32 Edad {get;set;}
Nombre      Property   System.String Nombre {get;set;}
Sexo        Property   System.String Sexo {get;set;}


PS C:\> $Faustino.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    Persona                                  System.Object

Y aquí termina este ladrillo -(|:oÞ