2.7.3. Herramientas de manipulación

Se tratan ahora programas que permiten alterar un flujo de texto y obtener otro modificado1.

tr

Permite intercambiar (traducir) en un flujo de texto una serie de caracteres por otros. A diferencia de otros comandos, no admite una argumento que indique un fichero como origen del flujo, por lo que siempre lee de la entrada estándar:

tr [<opciones>] <caracteres_originales> [<caracteres_nuevos>]

Un ejemplo simple es el siguiente:

$ echo "hola, caracola" | tr 'oa' '04'
h0l4, c4r4c0l4

Como se ve sustituye un carácter de la primera cadena por su correspondiente en la segunda. Ahora bien, si se incluye la opción -d, no es necesario indicar la segunda ristra de caracteres, porque lo que hace es, simplemente, eliminar los caracteres indicados:

$ echo "Pedro Sanchez" | tr -d 'aeiou'
Pdr Snchz

Dado el uso que tiene, esta orden no utiliza expresiones regulares. Ahora bien, sí permite indicar clases de caracteres, tal como se nombraron cuando se dieron las regez ERE:

$ echo "pansando a mayusculas" | tr '[:lower:]' '[:upper:]'
PASANDO A MAYUSCULAS

y también rangos:

$ echo "pansando a mayusculas" | tr 'a-z' 'A-Z'
PASANDO A MAYUSCULAS

Advertencia

La implementación de tr de que disponen los linux, no soporta utf-8. Por este motivo, las letras acentuadas no tendrán un buen tratamiento y veremos resoluciones absurdas como esta:

$ echo "ángel" | tr 'á' 'a'
aangel
cut

Como su nombre indica, cut permite seleccionar una parte de cada una de las líneas que componen un fichero. Qué criterio se seguirá para seleccionar qué interesa dependerá de qué opciones se le pasen. La sintaxis general es:

$ cut [<opciones>] [fichero1 [fichero2…]]

Como en otros casos, no expresar fichero alguno supondrá que el flujo se obtendrá de la entrada estándar.

Si lo que se pretende es seleccionar por posición, debemos usar la opción -c2. Por ejemplo:

$ echo "1234567890" | cut -c3-5
345

Para expresar el rango, puede usarse como se ha hecho (Inicio-Fin), pero tambíen indicando sólo el principio (Inicio-):

$ echo "1234567890" | cut -c5-
567890

o indicando sólo el final (-Final):

$ echo "1234567890" | cut -c-5
12345

Si expresamos sólo un número, entonces escogeremos exactamente ese carácter:

$ echo "1234567890" | cut -c5
5

También es posible seleccionar distintos rangos, separándolos por comas:

$ echo "1234567890" | cut -c1-2,4,6-8
124678

Si en cambio nos encontramos con líneas constituidas por campos a los que separa un carácter delimitador, entonces la selección es mejor hacerla a través de la opción -d que nos permite indicar el delimitador y -f que nos pormite indicar qué campos queremos seleccionar. Si no se indica delimitador, se sobrentiende que es la tabulación. Para expresar qué cambios se usa exactamente la misma sintaxis que con la opción -c:

$ echo "campo1,campo2,campo3" | cut -d, -f2
campo2

Un flujo que se presta enormemente al uso de cut con este criterio es el de definición de los usuarios o los grupos, cuyos campos están separados por dos puntos:

$ getent group | cut -d: -f1
[... Nombres de todos los grupos que existen en el sistema ...]

Cuando se usa este último criterio de selección, es conveniente saber que añadir la opción -s excluye las líneas que no contienen el delimitador.

sort

Ordena las líneas del flujo de datos entrante:

sort [<opciones>] [fichero1 [fichero2...]]

Si se especifica un fichero o varios, se tomará como datos entrantes los contenidos de los ficheros. Por ejemplo:

$ sort /etc/passwd

Ordena los usuarios locales según el nombre que tengan, puesto que cada línea de este fichero comienza con el nombre de un usuario (véase la gestión de usuarios). La ordenación de las líneas es alfabética. Si se quiere usar una ordenación numérica, puede añadirse la opción -n.

Otra opción interesante es -k que permite indicar el campo3 por el que se desea ordenador y -t que permite indicar el carácter de separación de campos, si éste no son espaciados:

$ sort -nk3 -t: /etc/passwd

En este caso, ordenamos por UID.

uniq

Elimina líneas repetidas del flujo de texto:

uniq [<opciones>] [<origen> [<destino>]]

Si no se especifica un origen se sobrentiende que este es la entrada estándar. Si no se sobrentiende un destino, se mostrará el resultado en la salida estándar.

Para que el comando identifique que las líneas son iguales es necesario que sean consecutivas, por lo que suele usarse en conjunción con sort para que este las ordene primero. La siguiente orden muestra las shells distintas que usan todos los usuarios definidos en el sistema:

$ getent passwd | egrep -o '[^:]+$' | sort | uniq
/bin/bash
/bin/false
/bin/sync
/usr/sbin/nologin
sed

sed es el acrónimo de Stream EDitor (o sea, editor de flujo) y es precisamente eso: un programa que recibe un flujo de caracteres y es capaz de alterarlo a través de las transformaciones que ordenemos. La sintaxis es la siguiente:

$ sed [opciones] <transformacion> [fichero]

Básicamente, se proporciona una fuente de datos, esto es, un fichero o la entrada estándar si no se especifica ninguno, y una cadena que define qué transformación se quiere hacer. Es posible también añadir algunas opciones que modifican la forma en que sed se comporta. En cualquier caso la que usaremos siempre es -r porque permite usar regex de tipo ERE, en vez de BRE.

Es un programa que permite hacer transformaciones bastante complicadas y cuyo manejo absoluto requiere bastante más que una pequeña reseña en unos apuntes. Así que nos limitaremos a realizar modificaciones sencillas. Lo que es imprescindible en cualquier caso es entender bien como funciona:

sed recibe el flujo de caracteres línea a línea y para cada línea aplica la transformación o las transformaciones que se le hayan ordenado; y así transformada es devuelta o no. Así continúa operando hasta llegar a la última de las líneas4.

Empecemos por lo más sencillo:

$ sed -r '' /etc/oasswd
[...  Contenido del fichero /etc/passwd ...]

Como la transformación es no hacer nada, sed no realiza ninguna transformación, y devuelve exactamente las mismas líneas que recibió. La consecuencia es que veremos en la pantalla el fichero original sin retocar. Esto es así, porque de modo predeterminado sed imprime la transformada. Sin embargo, si añadimos la opción -n, pasará a no imprimir a menos que explícitamente así que diga:

$ sed -nr '' /etc/passwd

No obtendremos absolutamente nada. Como podemos incluir en nuestra transformación p, que significa imprimir, podría haber escrito:

$ sed -nr 'p' /etc/passwd
[...  Contenido del fichero /etc/passwd ...]

Y habríamos obtenido el mismo efecto que con la primera orden.

Una de las transformaciones más socorridas es modificar la línea del siguiente modo:

s:<regex>:<texto_modificado>:[modificadores]

Por ejemplo:

$ sed -r 's:^:L.-:' /etc/passwd
 L.-root:x:0:0:root:/root:/bin/bash
 [... Resto del fichero modificado ...]

Añadirá al principio de línea los caracteres L.-. Las transformaciones pueden ser todo lo complicadas que nos permitan las expresiones regulares. Por ejemplo:

$ sed -r 's:(\w+):#\1#:' /etc/passwd
#root#:x:0:0:root:/root:/bin/bash
[... Resto del fichero modificado ...]

Como vemos hemos logrado rodear la primera palabra por dos almohadillas. Esto se debe a que la transformación consiste en capturar la palabra y sustituirla por una almohadilla, seguida por la propia palabra y otra almohadilla. Sin embargo no se han modificado el resto de palabras, esto es debido a que la sustitución acaba cuando se logra hacer una vez. Si queremos que la sustitución se repita a lo largo de toda la línea puede añadirse el modificador g:

$ sed -r 's:(\w+):#\1#:g' /etc/passwd
#root#:#x#:#0#:#0#:#root#:/#root#:/#bin#/#bash#
[... Resto del fichero modificado ...]

Es posible realizar dos transformaciones sobre las líneas, separándolas por un punto y coma:

$ sed -r 's:(\w+):#\1#: ; s:^:L.-:' /etc/passwd
L.-#root#:x:0:0:root:/root:/bin/bash
[... Resto del fichero modificado ...]

Sustituir no es la única acción que puede hacer sed. También puede borrar por completo la línea:

$ sed -r 'd' /etc/passwd

Que no imprimirá nada, obviamente, puesto que antes de devolver la línea la borramos. Esto resulta aparentemente inútil, pero no lo es, porque sed también permite indicar sobre qué líneas queremos realizar la transformación5:

$ sed -r '1d' /etc/oasswd
[... Todo el fichero excepto la primera línea ...]
$ sed -r '$d' /etc/passwd
[... Todo el fichero excepto la última línea ...]
$ sed -r '1,5d' /etc/passwd
[... Todo el fichero excepto las cinco primeras líneas ...]

Cuando lo que se quiere es realizar la acción sobre todas las líneas menos sobre las que se especifica, se añade una exclamación:

$ sed -r '1!d' /etc/oasswd
[... Sólo la primera línea ...]
$ sed -r '$d' /etc/passwd
[... Sólo la última línea ...]
$ sed -r '1,5!d' /etc/passwd
[... Sólo las primeras cinco primeras líneas ...]

Para seleccionar líneas también podemos usar expresiones regulares:

$ sed -r '/^u/d' /etc/passwd
[... No se muestran las líneas que empiezan por u ...]
$ sed -r '2,/^u/d' /etc/passwd
[... No se muestra desde la segunda líneas hasta la primera que empieza por u ...]

Si se quiere realizar más de una transformación sobre una selección de líneas, pueden usarse paréntesis:

$ sed -r '1,5{s:^:P: ; s:$:F:}' /etc/passwd
[ ... Sólo entre las líneas 1 y 5 se realizan las sustituciones ...]

Por último, otra acción que puede realizar es parar la edición antes de que acabe el flujo. Para ello existe q y Q. La diferencia entre una y otra es que Q acaba sin imprimir la línea pendiente:

$ sed -r '5q' /etc/passwd
[... Se imprimen las cinco primeras líneas ...]
$ sed -r '5Q' /etc/passwd
[... Se imprimen las cuatro primeras líneas ...]
awk6

Al igual que sed es un programa, cuyas capacidades exceden el propósito de estos apuntes. De hecho, es un lenguaje de programación completo. Nosotros, en cambio, le daremos un uso bastante limitado: lo utilizaremos para tratar ficheros de texto cuyas líneas estén constituidas por campos y en las que queramos realizar cambios. cut con su opción -f sirve para esto mismo, pero no nos permite hacer transformaciones en la línea. La sintaxis más básica de awk es la siguiente:

awk [<opciones>] <script-en-linea> [fichero]

Antes de continuar es pertinente advertir de que debian permite instalar dos variantes de awk: mawk, que es la que viene instalada por defecto, y es menos potente aunque más ligera; y gawk. La primera, al igual que pasa con tr no soporta utf-8.

awk opera igual que sed: lee línea a línea y dentro de cada una de ellas identifica los campos que existen. El delimitador es cualquier carácter de espaciado (el propio espacio o la tabulación), pero a diferencia de cut, los colapsa, de modo que si hay varios espacios o tabuladores seguidos los considera como uno sólo. Con respecto a esto define varias variables:

NR

Es el número de registro, es decir el número de línea.

NF

Es el número total de campos que tiene una línea.

$0

Almacena el contenido de una línea.

$1, $2, … , etc.

Almacena el contenido de cada campo.

Además, podemos cambiar el delimitador añadiendo la opción -F. Así, por ejemplo, para obtener los nombres de los grupos definidos en el sistema puede hacerse lo siguiente:

$ getent group | awk -F: '{print $1}'

La acción que realicemos debe escribirse entre llaves y en caso de querer hacer varias, todas estarán dentro de ellas y se separarán por un punto y coma. En particular, hemos hecho print $1 para imprimir (print) el primer campo. Esta acción se puee usar de forma que juxtapongamos varios argumentos:

$ getent group | awk -F: '{print NR ": " $1}'
1: root
2: daemon
[... Resto de grupos del sistema ...]

También es posible separar los argumentos con comas. En este caso, awk interpondrá entre un argumento y otro el carácter que tenga definido como separador de campos para la salida, que no tiene que coincidir con el separador de campos para la entrada. El predeterminado es el espacio de ahí que:

$ getent group | awk -F: '{print NR, $1}'
1 root
2 daemon
[... Resto de grupos del sistema ...]

separa el número de registro de del primer campo con un espacio, a pesar de que el caracter delimitador de entrada son los dos puntos. Tal delimitador de salida se almacena en el valor de la variable OFS, de modo que, si cambiamos su valor, cambiará la salida:

$ getent group | awk -F: -v OFS=".- " '{print NR, $1}'
1.- root
2.- daemon
[... Resto de grupos del sistema ...]

awk dispone de bastantes funciones que permiten alterar los valores de las variables que se muestran, algunas de las cuales permiten hacer sustituciones como las vistas con sed. Basta consultar un buen manual para conocerlas. Para ilustrar esto, podemos usar toupper que pasa a mayúsculas:

$ getent group | awk -F: '{print NR, toupper($1) }'
1 ROOT
2 DAEMON
[... etc ...]

Para culminar la ínfima aproximación a awk, debe también explicarse cómo filtrar líneas. Esto se logra anteponiendo una expresión regular rodeada por barras:

$ getent group | awk -F: '/^[^r]/ {print NR, $1 }'
2 daemon
3 bin
[... etc ...]

En este caso la expresión regular selecciona líneas que no comiencen con la letras «r», de ahí que la primera línea no aparezca como resultado. Escrito de esta forma, la expresión regular se aplica a toda la línea, esto es, al valor de la variable $0. Si se quiere filtrar sólo por el valor de un campo, puede usarse la siguiente sintaxis:

$ getent group | awk -F: '$4 ~ /./ {print NR, $0 }'
5 adm:x:4:syslog,usuario
18 cdrom:x:24:usuario
21 sudo:x:27:usuario
23 dip:x:30:usuario
35 plugdev:x:46:usuario
47 lpadmin:x:108:usuario
58 sambashare:x:118:usuario

En este caso, sólo hemos mostrado las líneas que contienen algo en el cuerto campo. Como este, además, es el último, podríamos haberlo resuelto también así:

$ getent group | awk -F: '$NF ~ /./ {print NR, $0 }'
[... Obtenemos la misma salida ...]

por cuanto NF vale 4 y $NF equivale a $4.

Nota

Para una información más sistemática consulte awk en una línea.

Notas al pie

1

En puridad, ya grep y wc permiten esto: el primero filtra líneas, por lo que se obtiene un flujo distinto y el segundo convierte el flujo en una pequeña estadística sobre el mismo.

2

También existe la opción -b que selecciona por posición, pero en vez de identificar caracteres identifica bytes. Recuérdese que hay sistemas de codificación (y UTF-8 está entre ellos) en que cada carácter no ocupa un byte (repásese el ejemplo dado al explicar tr).

3

En realidad, -k escoger algo más complejo que un simple campo (consulte sort(1)). Por lo pronto permite escoger hasta dos campos como criterio:

$ sort -nk4,3 /etc/passwd
4

Como ya se ha dicho sed es bastante complejo. Este es su comportamiento, pero permite leer la siguiente línea antes de haber acabado de transformar la presente, o almacenar líneas transformadas y dejarlas reservadas para devolverlas más tarde. No trataremos estas capacidades.

5

Para notar la última línea se usa el dólar. Ahora bien, aunque es posible saber que se está en la última línea, es imposible conocer que es está en la penúltima, es decir, expresiones como $-1 no son válidas. Esto es debido al modo en que actúa el programa: al ir leyendo línea a línea, es incapaz de saber las líneas que aún le quedan por leer.

6

La versión de awk instalada en Debian es mawk. Existe otra más potente pero más lenta, con el nombre de gawk