Encontrando cosas
Overview
Enseñando: 15 min
Prácticas: 0 minPreguntas
¿Cómo puedo encontrar archivos?
¿Cómo puedo encontrar cosas dentro de archivos?
Objetivos
Usar
grep
para seleccionar líneas de archivos de texto que coincidan con patrones simples.Use
find
para encontrar archivos cuyos nombres coincidan con patrones simples.Usar la salida de un comando como parámetros de otro comando.
Explicar lo que se entiende por archivos de ‘texto’ y ‘binarios’, y por qué muchas herramientas comunes no manejan bien estos últimos.
De la misma manera que muchos de nosotros ahora usamos “googlear” como un verbo que significa “encontrar”, los programadores de Unix usan cotidianamente la palabra “grep”. “grep” es una contracción de “global/regular expression/print” (global/expresión regular/imprimir), una secuencia común de operaciones en los primeros editores de texto de Unix. También es el nombre de un programa de línea de comandos muy útil.
El comando grep
encuentra e imprime líneas en archivos que coinciden con un patrón.
Para nuestros ejemplos,
usaremos un archivo que contenga tres haikus publicados en
1998 en la revista Salon. Para este conjunto de ejemplos,
Vamos a estar trabajando en el subdirectorio “writing” :
$ cd
$ cd writing
$ cat haiku.txt
The Tao that is seen
Is not the true Tao, until
You bring fresh toner.
With searching comes loss
and the presence of absence:
"My Thesis" not found.
Yesterday it worked
Today it is not working
Software is like that.
Para siempre, o Cinco Años
No hemos vinculado a los haikus originales porque parecen ya no estar en el sitio de Salon. Como Jeff Rothenberg dijo, “La información digital dura para siempre — o cinco años, lo que ocurra primero.”
Busquemos líneas que contengan la palabra “no”:
$ grep not haiku.txt
Is not the true Tao, until
"My Thesis" not found
Today it is not working
En este ejemplo, not
es el patrón que estamos buscando.
Es bastante simple:
cada carácter alfanumérico coincide con sí mismo.
Después del patrón viene el nombre o los nombres de los archivos que estamos buscando.
La salida son las tres líneas del archivo que contienen las letras “not”.
Vamos a probar un patrón distinto: “The”.
$ grep The haiku.txt
The Tao that is seen
"My Thesis" not found.
Esta vez, las dos líneas que incluyen las letras “The” forman parte del producto de la búsqueda. Sin embargo, una instancia de esas letras está contenida en una palabra más grande, “Thesis”.
Para restringir los aciertos a las líneas que contienen la palabra “The” por sí sola,
podemos usar grep
con el indicador -w
.
Esto limitará los coincidencias con los límites de las palabras.
$ grep -w The haiku.txt
The Tao that is seen
Notar que una “word boundary” incluye el comienzo y el final de una línea, no solamente aquellas letras rodeadas de espacios.
A veces, queremos buscar una frase en vez de una sola palabra. Esto puede hacerse fácilmente con grep
usando la frase entre comillas.
$ grep -w "is not" haiku.txt
Today it is not working
Hemos visto que no requerimos comillas alrededor de palabras simples, pero es útil utilizar comillas cuando se buscan varias palabras consecutivas. También ayuda a facilitar la distinción entre el término o frase de búsqueda y el archivo que se está buscando. Utilizaremos cotizaciones en los ejemplos restantes.
Otra opción útil es -n
, que numera las líneas que coinciden:
$ grep -n "it" haiku.txt
5:With searching comes loss
9:Yesterday it worked
10:Today it is not working
Aquí, podemos ver que las líneas 5, 9 y 10 contienen las letras “it”.
Podemos combinar opciones (es decir, flags) como lo hacemos con otros comandos de Unix.
Por ejemplo, vamos a encontrar las líneas que contienen la palabra “the”. Podemos combinar
la opción -w
para encontrar las líneas que contienen la palabra “the” con -n
para numerar las líneas que coinciden:
$ grep -n -w "the" haiku.txt
2:Is not the true Tao, until
6:and the presence of absence:
Ahora queremos usar la opción -i
para hacer que nuestra búsqueda sea insensible a mayúsculas y minúsculas:
$ grep -n -w -i "the" haiku.txt
1:The Tao that is seen
2:Is not the true Tao, until
6:and the presence of absence:
Ahora, queremos usar la opción -v
para invertir nuestra búsqueda, es decir, queremos que
obtener las líneas que NO contienen la palabra “the”.
$ grep -n -w -v "the" haiku.txt
1:The Tao that is seen
3:You bring fresh toner.
4:
5:With searching comes loss
7:"My Thesis" not found.
8:
9:Yesterday it worked
10:Today it is not working
11:Software is like that.
grep
tiene muchas otras opciones. Para averiguar cuáles son, podemos escribir:
$ grep --help
Usage: grep [OPTION]... PATTERN [FILE]...
Search for PATTERN in each FILE or standard input.
PATTERN is, by default, a basic regular expression (BRE).
Example: grep -i 'hello world' menu.h main.c
Regexp selection and interpretation:
-E, --extended-regexp PATTERN is an extended regular expression (ERE)
-F, --fixed-strings PATTERN is a set of newline-separated fixed strings
-G, --basic-regexp PATTERN is a basic regular expression (BRE)
-P, --perl-regexp PATTERN is a Perl regular expression
-e, --regexp=PATTERN use PATTERN for matching
-f, --file=FILE obtain PATTERN from FILE
-i, --ignore-case ignore case distinctions
-w, --word-regexp force PATTERN to match only whole words
-x, --line-regexp force PATTERN to match only whole lines
-z, --null-data a data line ends in 0 byte, not newline
Miscellaneous:
... ... ...
Comodines
La verdadera ventaja de
grep
no proviene de sus opciones; viene de el hecho de que los patrones pueden incluir comodines. (El nombre técnico para estos son expresiones regulares, que es lo que significa el “re” en “grep”.) Las expresiones regulares son complejas y poderosas; si quieres realizar búsquedas complejas, mira la lección En nuestro sitio web. Como prueba, podemos encontrar líneas que tienen un ‘o’ en la segunda posición como esto:$ grep -E '^.o' haiku.txt
You bring fresh toner. Today it is not working Software is like that.
Utilizamos el indicador
-E
y ponemos el patrón entre comillas para evitar que el shell intente interpretarlo. (Si el patrón contenía un*
, por ejemplo, el shell intentaría expandirlo antes de ejecutar grep.)^
en el patrón de búsqueda ancla la coincidencia al inicio de la línea. El.
coincide con un solo carácter (como?
en el shell), mientras que elo
coincide con una ‘o’ real.
Mientras grep
encuentra líneas en los archivos,
El comando find
busca los archivos.
Una vez más,
tiene muchas opciones;
Para mostrar cómo funcionan las más simples, utilizaremos el árbol de directorios que se muestra a continuación.
El directorio writing
de Nelle contiene un archivo llamado haiku.txt
y tres subdirectorios:
thesis
(que contiene un archivo tristemente vacío, empty-draft.md
),
data
(que contiene dos archivos LittleWomen.txt
, one.txt
y two.txt
),
un directorio tools
que contiene los programas format
y stats
,
y un subdirectorio llamado old
, con un archivo oldtool
.
Para nuestro primer comando, ejecutemos `find ‘.
$ find .
.
./data
./data/one.txt
./data/LittleWomen.txt
./data/two.txt
./tools
./tools/format
./tools/old
./tools/old/oldtool
./tools/stats
./haiku.txt
./thesis
./thesis/empty-draft.md
Como siempre,
el .
por sí mismo significa el directorio de trabajo actual,
que es donde queremos que empiece nuestra búsqueda.
La salida de find
es el nombre de cada archivo y directorio
dentro del directorio de trabajo actual.
Esto puede parecer inútil al principio, pero find
tiene muchas opciones
para filtrar la salida y en esta lección vamos a descubrir algunas
de ellas.
La primera opción en nuestra lista es
-type d
que significa “cosas que son directorios”.
Por supuesto que
la salida de find
serán los nombres de los cinco directorios en nuestro pequeño árbol
(incluyendo .
):
$ find . -type d
./
./data
./thesis
./tools
./tools/old
Si cambiamos -type d
por -type f
,
recibimos una lista de todos los archivos en su lugar:
$ find . -type f
./haiku.txt
./tools/stats
./tools/old/oldtool
./tools/format
./thesis/empty-draft.md
./data/one.txt
./data/LittleWomen.txt
./data/two.txt
Ahora tratemos de buscar por nombre:
$ find . -name *.txt
./haiku.txt
Esperábamos que encontrara todos los archivos de texto,
Pero sólo imprime ./haiku.txt
.
El problema es que el shell amplía los caracteres comodín como *
antes de ejecutar los comandos.
Dado que *.txt
en el directorio actual se expande a haiku.txt
,
El comando que corrimos era:
$ find . -name haiku.txt
find
hizo lo que pedimos; Sólo pedimos la cosa equivocada.
Para conseguir lo que queremos,
vamos a hacer lo que hicimos con grep
:
ponga * txt
en comillas simples para evitar que el shell expanda el comodín *
.
De esta manera,
find
obtiene realmente el patrón *.txt
, no el nombre de archivo expandido haiku.txt
:
$ find . -name '*.txt'
./data/one.txt
./data/LittleWomen.txt
./data/two.txt
./haiku.txt
Listado vs. Búsqueda
ls
yfind
se pueden usar para hacer cosas similares dadas las opciones correctas, pero en circunstancias normales,ls
enumera todo lo que puede, mientras quefind
busca cosas con ciertas propiedades y las muestra.
Como mencionamos anteriormente,
el poder de la línea de comandos reside en la combinación de herramientas.
Hemos visto cómo hacerlo con tuberías;
veamos otra técnica.
Como acabamos de ver,
find . -name '*.txt'
nos da una lista de todos los archivos de texto dentro o más abajo del directorio actual.
¿Cómo podemos combinar eso con wc -l
para contar las líneas en todos esos archivos?
La forma más sencilla es poner el comando find
dentro de $()
:
$ wc -l $(find . -name '*.txt')
11 ./haiku.txt
300 ./data/two.txt
21022 ./data/LittleWomen.txt
70 ./data/one.txt
21403 total
Cuando el shell ejecuta este comando,
lo primero que hace es ejecutar lo que esté dentro del $()
.
Luego reemplaza la expresión $()
con la salida de ese comando.
Dado que la salida de find
es los cuatro nombres de directorio ./data/one.txt
, ./data/LittleWomen.txt
, ./data/two.txt
, y ./haiku.txt
,
el shell construye el comando:
$ wc -l ./data/one.txt ./data/LittleWomen.txt ./data/two.txt ./haiku.txt
que es lo que queríamos.
Esta expansión es exactamente lo que hace el shell cuando se expanden comodines como *
y ?
,
pero nos permite usar cualquier comando que deseemos como nuestro propio “comodín”.
Es muy común usar find
y grep
juntos.
El primero encuentra archivos que coinciden con un patrón;
el segundo busca líneas dentro de los archivos que coinciden con otro patrón.
Aquí, por ejemplo, podemos encontrar archivos PDB que contienen átomos de hierro
buscando la cadena “FE” en todos los archivos .pdb
por encima del directorio actual:
$ grep "FE" $(find .. -name '*.pdb')
../data/pdb/heme.pdb:ATOM 25 FE 1 -0.924 0.535 -0.518
Archivos binarios
Nos hemos centrado exclusivamente en encontrar cosas en archivos de texto. ¿Y si sus datos se almacenan como imágenes, en bases de datos, o en algún otro formato? Una opción sería extender herramientas como
grep
para manejar esos formatos. Esto no ha sucedido, y probablemente no lo hará, porque hay demasiados formatos disponibles.La segunda opción es convertir los datos en texto, o extraer los bits de texto de los datos. Este es probablemente el enfoque más común, ya que sólo requiere generar una herramienta por formato de datos (para extraer información). Por un lado, facilita realizar las cosas simples. Por el contrario, las cosas complejas son generalmente imposibles. Por ejemplo, es bastante fácil escribir un programa que extraerá X e Y dimensiones de los archivos de imagen para que podamos después usar
grep
, pero ¿cómo escribir algo para encontrar valores en una hoja de cálculo cuyas celdas contenían fórmulas?La tercera opción es reconocer que el shell y el procesamiento de texto tiene sus límites, y utilizar otro lenguaje de programación. Sin embargo, el shell te seguirá siendo útil para realizar tareas de procesamiento diarias.
El shell de Unix es más antiguo que la mayoría de las personas que lo usan. Ha sobrevivido tanto tiempo porque es uno de los ambientes de programación más productivos jamás creados — quizás incluso el más productivo. Su sintaxis puede ser críptica, pero las personas que lo dominan pueden experimentar con diferentes comandos interactivamente, y luego utilizar lo que han aprendido para automatizar su trabajo. Las interfaces gráficas de usuario pueden ser más sencillas al principio, pero el shell sigue invicto en segunda instancia. Y como Alfred North Whitehead escribió en 1911, “La civilización avanza extendiendo el número de operaciones importantes que podemos realizar sin pensar en ellas.”
Utilizando
grep
Haciendo referencia a
haiku.txt
presentado al inicio de este tema, qué comando resultaría en la salida siguiente:and the presence of absence:
grep "of" haiku.txt
grep -E "of" haiku.txt
grep -w "of" haiku.txt
grep -i "of" haiku.txt
find
Comprensión de lectura de pipelineEscriba un breve comentario explicativo para el siguiente script de shell:
wc -l $(find . -name '*.dat') | sort -n
Buscando coincidencias y restando
La bandera
-v
agrep
invierte la coincidencia de patrones, de modo que sólo las líneas que no coinciden con el patrón se imprimen. Dado esto ¿cuál de los siguientes comandos encontrarán todos los archivos en/data
cuyos nombres terminan enose.dat
(por ejemplo,sucrose.dat
omaltose.dat
), pero no contienen la palabratemp
?
find /data -name '*.dat' | grep ose | grep -v temp
find /data -name ose.dat | grep -v temp
grep -v "temp" $(find /data -name '*ose.dat')
- Ninguna de las anteriores.
Seguimiento de una especie
Leah tiene varios cientos de archivos de datos guardados en un directorio, cada uno de los cuales tiene el formato siguiente:
2013-11-05,deer,5 2013-11-05,rabbit,22 2013-11-05,raccoon,7 2013-11-06,rabbit,19 2013-11-06,deer,2
Quiere escribir un script de shell que tome un directorio y una especie como parámetros de línea de comandos y devuelva un archivo llamado
species.txt
que contenga una lista de fechas y el número de individuos de esa especie observados en esa fecha, como este archivo de números de conejos:2013-11-05,22 2013-11-06,19 2013-11-07,18
Pon estos comandos y tuberías en el orden correcto para lograrlo:
cut -d : -f 2 > | grep -w $1 -r $2 | $1.txt cut -d , -f 1,3
Sugerencia: use
man grep
para buscar cómo seleccionar texto recursivamente en un directorio usando grep yman cut
para seleccionar más de un campo en una línea. Un ejemplo es el siguiente archivodata-shell/data/animal-counts/animals.txt
Solución
grep -w $1 -r $2 | cut -d : -f 2 | cut -d , -f 1,3 > $1.txt
Puedes llamar al script de la siguiente forma:
$ bash count-species.sh bear .
Mujercitas
Tú y tu amigo, que acaban de terminar de leer Little Women de Louisa May Alcott, y tienen una desacuerdo. De las cuatro hermanas en el libro, Jo, Meg, Beth, y Amy, tu amigo piensa que Jo era la más mencionada. Tú, sin embargo, estás seguro de que era Amy. Afortunadamente tu tienen un archivo
LittleWomen.txt
que contiene el texto completo de la novela. Utilizando un buclefor
, ¿cómo tabularían el número de veces que cada una de las cuatro hermanas es mencionada?Sugerencia: una solución sería emplear los comandos
grep
ywc
y un|
, mientras que otra podría utilizar opciones degrep
. Normalmente hay más de una forma para solucionar una tarea de programación. Cada solución es seleccionada de acuerdo a la combinación entre cuál es la que produce el resultado correcto, con mayor elegancia, de la manera más clara y con mayor velocidad.Solución
for sis in Jo Meg Beth Amy do echo $sis: grep -ow $sis LittleWomen.txt | wc -l done
Alternativa, un poco inferior:
for sis in Jo Meg Beth Amy do echo $sis: grep -ocw $sis LittleWomen.txt done
Esta solucón es inferior porque
grep -c
solamente reporta el número de las líneas. El número total de coincidencias es reportado por este método va a ser más bajo si hay más de una coincidencia por línea.
Búsqueda de archivos con propiedades diferentes
El comando
find
puede recibir varios otros criterios conocidos como “tests” para localizar archivos con atributos específicos, como tiempo de creación, tamaño, permisos o dueño. Explora esto conman find
y luego escribe un solo comando para encontrar todos los archivos en o debajo del directorio actual que han sido modificados por el usuarioahmed
en las últimas 24 horas.Sugerencia 1: necesitarás utilizar tres tests:
-type
,-mtime
y-user
.Sugerencia 2: El valor de
-mtime
tendrá que ser negativo — ¿por qué?Solución: Asumiendo que el directorio de inicio de Nelle es nuestro directorio en el que trabajamos, podemos usar:
$ find ./ -type f -mtime -1 -user ahmed
Puntos Clave
find
encuentra archivos con propiedades específicas que coinciden con patrones especificados.
grep
selecciona líneas en archivos que coinciden con patrones especificados.
--help
es un indicador soportado por muchos comandos bash, y programas que se pueden ejecutar desde dentro de Bash, para mostrar más información sobre cómo usar estos comandos o programas.
man command
muestra la página de manual de un comando dado.
$(comando)
contiene la salida de un comando.