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.
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.
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).
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).
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 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.
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 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
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Þ