2.8.3. Herramientas de manipulación

Se tratan ahora programas que permiten alterar un flujo de texto y obtener otro modificado[1].

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
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 campo[2] 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
paste

Es una herramienta que permite mezclar varios archivos, de modo que las líneas del archivo resultante están formadas por la yuxtaposición de las líneas correspondientes de los archivos fuente. Por defecto, se usa como caracter de yuxtaposción el tabulador. Un ejemplo ayuda enormemente a entender su utilidad:

$ seq 1 5 > numeros
$ echo -e 'a\nb\nc\nd' > letras
$ echo -e 'i\nii\niii' > romanos
$ paste numeros letras romanos
1       a       i
2       b       ii
3       c       iii
4       d
5

El carácter delimitador puede modificarse con la opción -d:

$ paste -d, numeros letras romanos
1,a,i
2,b,ii
3,c,iii
4,d,
5,,

También puede obtenerse el resultado traspuesto si añadimos la opción -s:

$ paste -s numeros letras romanos
1       2       3       4       5
a       b       c       d
i       ii      iii

Cuando no se especifica fichero alguno, la orden lee de la entrada estándar:

$ seq 1 5 | paste -s -d,
1,2,3,4,5
csplit

Es una orden que permite dividir un archivo de entrada en varios de salida, atendiendo a ciertas líneas que actúan como separador entre sus partes. Por ejemplo, imaginemos ente archivo de entrada:

Primera
parte de este
archivo
%
Segunda
parte del
archivo de
texto
%
Tercera

En este caso, el archivo tiene tres partes separadas por una línea que incluye únicamente el carácter procentual «%». csplit nos permite generar las tres partes por separado:

$ csplit entrada.txt '/^%$/' '{*}'

Este es un ejemplo muy simple en que, simplemente, indicamos:

  • el archivo de entrada

  • cuál es la línea separadora mediante una expresión regular (de tipo BRE)

  • generar tantos archivos como partes haya ({*}), porque de lo contrario la orden se limitará a generar dos archivos separados por el primer separador que encuentre.

El resultado es que se generan tres archivos llamados xx00, xx01 y xx02. Cada archivo además contendrá la propia línea separadora. Por tanto, xx02 será[3]:

%
Tercera

Hay, no obstante, varias opciones interesantes con la que podemos alterar el comportamiento:

$ csplit -z --suppress-matched -fparte -b%02d.txt caca.txt '/^%$/' '{*}'

En este caso, hemos añadido lo siguiente:

  • Hemos eliminado la generación de archivos vacios. Esto tiene mucha utilidad si la primera línea del archivo de entrada es una línea separadora, porque eso supondría que xx00 resulta un archivo vacío.

  • Hemos eliminado la línea separadora de los archivos de salida, lo cual dependiendo del caso puede interesarnos o no.

  • Hemos modificado los nombres de los archivos de salida (con -f y -b con lo que ahora los archvios de salida se llamarán parte00.txt, parte01.txt, etc.

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 -c[4]. 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.

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íneas[5].

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ón[6]:

$ 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 ...]
awk[7]

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.

¿Qué herramiento uso?

Notas al pie