Ahora que sabemos editar un vídeo a partir de una secuencia de fotogramas con ffmpeg nos encontramos con que para poder hacerlo, los fotogramas han de estar numerados correlativamente y todos con el mismo número de dígitos en el numeral para que los ordene bien en el vídeo, lo cual es sencillo, pero si lo automatizamos, mejor.
Aquí os dejo la función que utilizo para ello:
function devuelvefotograma()
{
# Función que recibe el numeral del último fotograma creado y devuelve el fotograma a utilizar como imagen destino por convert
esentero='^[0-9]+$'
numerofotograma=$1
if [ -z $numerofotograma ] || ! [[ $numerofotograma =~ $esentero ]]
then
let numerofotograma=1
fi
if [ $numerofotograma -lt 10 ]
then
fotograma="fotograma000"$numerofotograma".png"
elif [ $numerofotograma -ge 10 ] && [ $numerofotograma -lt 100 ]
then
fotograma="fotograma00"$numerofotograma".png"
elif [ $numerofotograma -ge 100 ] && [ $numerofotograma -lt 1000 ]
then
fotograma="fotograma0"$numerofotograma".png"
else
fotograma="fotograma"$numerofotograma".png"
fi
echo $fotograma;
}
Primero defino la función y le llamo devuelvefotograma, ya que me gusta que el nombre de una función sea descriptivo, así que suelo usar un imperativo que indique lo que quiero que haga.
Una descripción de qué va a hacer la función.
Y comienzo la función diciéndole que $numerofotograma (también un nombre que deje claro qué va a almacenar esa variable) tome el valor de $1, es decir, del primer parámetro que le pasamos a la función. Y no es que no me guste usar el nombre de la variable $1, sino que $1 es genérico para todas las funciones y $numerofotograma es más sencillo de entender (sí, hay que teclear un poco más, pero a la hora de mantener un script es mejor si el nombre de las variables indican qué contienen. Y si tienes configurado en tu editor de textos que al pulsar tabulador complete los nombres de las funciones, tampoco tienes que teclear mucho más).
Le indico también que la variable $esentero sea una expresión regular que diga que desde el comienzo hasta el final sean números ('^[0-9]+$').
Compruebo si no tiene contenido en la variable ([ -z $numerofotograma ]) o (||) no es numérico (! [[ $numerofotograma =~ $esentero ]]) y si no tiene nada la variable o no es numérica, le soy el valor 1. Ya sé que puede sobreescribir fotogramas al hacerlo así, pero prefiero darle un valor a la variable que me de error la función. A gustos.
Pregunto si $numerofotograma es menor que 10 con [ $numerofotograma -lt 10 ], es decir, si únicamente tiene un carácter. Si es así, le añado tres ceros. Por ejemplo, si el número del fotograma es 1, le digo que sea 0001.
Si es igual o mayor que 10 ([ $numerofotograma -ge 10 ]) y además (&&), es menor que cien ([ $numerofotograma -lt 100 ]) es que tiene dos carácteres, así que le añado dos ceros. Si tiene tres carácteres es que va de 100 a 999, así que le digo que si es igual o mayor que cien y menor que mil, que añada un 0.
Y si es mayor que 1000 es que tiene 4 dígitos o más. Y no añado ningún cero. Alguien podría decir que qué pasa si hay más de 9.999 fotogramas. Sencillamente, que no uso esta función, ya que usaría la opción de codificar el vídeo al vuelo. Esta función la uso para generar y ver los fotogramas antes de renderizar un vídeo. Si un vídeo tiene más de 9.999 fotogramas puede ocupar unos 70 Gb de espacio en disco. Y no voy a visualizar individualmente tantos fotogramas.
La opción de visualizar fotograma a fotograma es para secuencias cortas en las que hay detalles importantes o una muestra para ver cómo quedan los fotogramas antes de hacer el vídeo.
Por último, devuelvo $fotograma.
Podemos definir la variable $numerofotograma o no. Si no la inciamos, la función la inicializa a 1. Si la queremos inicializar, por hacer un script más elegante, lo podemos hacer con:
let numerofotograma=1
Invocamos a la función pasándole el número de fotograma como parámetro y nada más saber el fotograma a utilizar, le decimos que le sume 1. Podemos hacerlo más tarde, pero es más fácil que se nos olvide si lo dejamos para después.
fotograma=$(devuelvefotograma $numerofotograma)
let numerofotograma++
Y ya podemos usar la variable fotograma como imagen de destino con convert.
#!/bin/bash
function devuelvefotograma()
{
# Función que recibe el numeral del último fotograma creado y devuelve el fotograma a utilizar como imagen destino por convert
esentero='^[0-9]+$'
numerofotograma=$1
if [ -z $numerofotograma ] || ! [[ $numerofotograma =~ $esentero ]]
then
let numerofotograma=1
fi
if [ $numerofotograma -lt 10 ]
then
fotograma="fotograma000"$numerofotograma".png"
elif [ $numerofotograma -ge 10 ] && [ $numerofotograma -lt 100 ]
then
fotograma="fotograma00"$numerofotograma".png"
elif [ $numerofotograma -ge 100 ] && [ $numerofotograma -lt 1000 ]
then
fotograma="fotograma0"$numerofotograma".png"
else
fotograma="fotograma"$numerofotograma".png"
fi
echo $fotograma;
}
for i in $(seq 1 50)
do
fotograma=$(devuelvefotograma $numerofotograma)
let numerofotograma++
echo "El fotograma es: "$fotograma
done
Este es un script de control, para ver que devuelve bien el nombre del fotograma. Usa cualquier parámetro de convert para generar los fotogramas con las modificaciones que desees.
Ya hemos visto que $RANDOM%$limitesuperior no funciona.
Hemos visto que $RANDOM%1 para hacer tiradas "a cara o cruz" no funciona.
Y del mismo modo que deberemos sospechar (y no apostar) si vemos que un trilero siempre saca el mismo resultado lanzando una moneda, cuando lanza un dado puede que haga también trampa. Los dados se pueden cargar y hacer que un lado pese más, por lo que hay más posibilidades de que ese lado sea el que se apoye en la mesa y, por lo tanto, sea el lado apostado el que quede arriba. Pasa lo mismo con $RANDOM:
#!/bin/bash
let nocoincide=0
for i in $(seq 1 1000)
do
for x in $(seq 1 1000)
do
let numero=$((RANDOM%$x))
if [ $numero -lt 10 ]
then
echo $numero >> coincidencias.txt
fi
done
done
for i in $(seq 0 9)
do
echo "Veces que se repite "$i
fgrep -o $i coincidencias.txt | wc -l
done
El resultado que me ha dado es (cada vez que lo probemos saldŕa algo distinto, ya que son números aleatorios):
Veces que se repite 0
7559
Veces que se repite 1
6668
Veces que se repite 2
5941
Veces que se repite 3
5730
Veces que se repite 4
5387
Veces que se repite 5
5219
Veces que se repite 6
5053
Veces que se repite 7
4810
Veces que se repite 8
4769
Veces que se repite 9
4638
Cuanto más pequeño es el número, más veces se repite. Curioso caso de "aleatoriedad". ¿No os resulta sospechoso? A mi, sí. Por lo que voy a compararlo con otros números. Y antes, voy a borrar coincidencias.txt para evitar viciar resultados:
rm coincidencias.txt
Y ahora, el script:
#!/bin/bash
let nocoincide=0
for i in $(seq 1 1000)
do
for x in $(seq 1 1000)
do
let numero=$((RANDOM%$x))
if [ $numero -lt 10 ]
then
echo $numero >> coincidencias.txt
elif [ $numero -eq 100 ]
then
echo $numero >> coincidencias.txt
elif [ $numero -eq 500 ]
then
echo $numero >> coincidencias.txt
elif [ $numero -eq 750 ]
then
echo $numero >> coincidencias.txt
elif [ $numero -eq 999 ]
then
echo $numero >> coincidencias.txt
fi
done
done
for i in $(seq 0 9)
do
echo "Veces que se repite "$i":"
fgrep -o $i coincidencias.txt | wc -l
done
echo "Datos de control"
for i in 100 500 750 999
do
echo "Veces que se repite "$i":"
fgrep -o $i coincidencias.txt | wc -l
done
Y el resultado:
Veces que se repite 0:
13717
Veces que se repite 1:
8694
Veces que se repite 2:
5986
Veces que se repite 3:
5651
Veces que se repite 4:
5389
Veces que se repite 5:
6259
Veces que se repite 6:
5029
Veces que se repite 7:
5339
Veces que se repite 8:
4796
Veces que se repite 9:
4701
Datos de control
Veces que se repite 100:
2296
Veces que se repite 500:
687
Veces que se repite 750:
277
Veces que se repite 999:
0
Ummmm... Como digo, algo que es aleatorio puede cambiar el resultado. Pero si siempre que ejecutamos el script nos da el mismo patrón, que es que cuanto más pequeño sea el número, más veces sale, es que esa aleatoriedad está tan viciada con los dados cargados de un trilero. Y es que $numero en $RANDOM%$numero no indica el límite superior sino el divisor de un dividendo aleatorio en una instrucción en la que le pedimos que nos indique el resto.
Ni más, ni menos.
En otro artículo ya comenté que para hacer una especie de "cara o cruz" en el que de forma aleatoria nos muestre 0 o 1, $RANDOM%1 no funciona, ya que siempre da 0. En un comentario me decían que dividir una cantidad entre 1 no da decimales. Es cierto, nunca podrá haber rendondeo a la alza porque nunca podrá haber redondeo y, por lo tanto, nunca podrá salir la cantidad indicada como límite superior del rango.
Voy a hacer un script en el que saquemos muchos números aleatorios. Por ejemplo, que mil veces haga una tirada aleatoria con resultados del 0 al 1.000.
#!/bin/bash
for i in $(seq 1 1000)
do
for x in $(seq 1 1000)
do
let numero=$((RANDOM%$x))
if [ $numero -eq $x ]
then
echo $x
fi
done
done
Al ejecutarlo no sale nada. Y repito, y repito, y repito la ejecución del script. Siempre sin ningún resultado. ¿Me habré equivocado? Voy a acumular las veces que el resultado no coincide con el límite superior:
#!/bin/bash
let nocoincide=0
for i in $(seq 1 1000)
do
for x in $(seq 1 1000)
do
let numero=$((RANDOM%$x))
if [ $numero -eq $x ]
then
echo $x
else
let nocoincide++
fi
done
done
echo "Han salido "$nocoincide" coincidencias con el límite superior"
Ahora el resultado siempre es el mismo:
Han salido 1000000 coincidencias con el límite superior
Pues igual no he sido yo el que me he equivocado. Aunque es cierto que al ser aleatorio puede que ese número no salga. Pero si varias veces repito un millón de tiradas y nunca coincide, algo raro pasa. Quizá lo raro es que quien afirma que para sacar un número aleatorio entre 0 y $x se puede conseguir con let numero=$((RANDOM%$x))
se equivoca... o que nunca se ha parado a comprobarlo.
Cuando queremos escribir en un fichero de texto desde un script de Bash, tenemos dos opciones:
Que queramos sobreescribir el fichero con el texto que le enviemos
Que queramos anexar el texto a un fichero ya existente
La primera opción la haremos con:
echo $variable > fichero.txt
La segunda opción, con >>:
echo $variable >> fichero.txt
Ejemplos
Si queremos escribir únicamente el último resultado:
#!/bin/bash
for i in $(seq 1 100)
do
echo $i > numeros.txt
done
Si queremos un fichero en el que se almacene una lista de números del uno al cien:
#!/bin/bash
for i in $(seq 1 100)
do
echo $i >> numeros.txt
done
Si queremos un número aleatorio en Bash, tenemos la función de Bash RANDOM. Es una función del intérprete de comandos, por lo que no lo podemos ejecutar como comando, sino que si queremos desde la línea de comandos un número aleatorio, deberemos escribir:
echo $RANDOM
Esto nos dará un número aleatorio entre 0 y 32.768 (2 elevado a 15).
Pero, ¿qué pasa si queremos delimitar el rango en el que queremos el número aleatorio?
Podemos dividirlo por el límite superior y, al devolver el módulo, siempre será, como máximo, el número indicado menos uno. En muchos artículos y tutoriales que dicen que el número que indicamos después del símbolo de porcentaje es el límite superior, pero esto no es así.
Veamos un caso extremo:
#!/bin/bash
contador=0
for i in $(seq 1 100000)
do
let numero=$((RANDOM%1))
if [ $numero -eq 1 ]
then
let contador=$contador+1
fi
done
echo $contador
Le digo que haga un bucle cien mil veces y que calcule un número aleatorio entre el 0 y el 1. Que cuente cuantas veces sale 1 y muestre el número de veces que ha salido 1.
El resultado siempre es 0.
Así pues, cuando queráis un número aleatorio con un rango, sumadle uno al rango superior que le indiquéis a $RANDOM.
Vamos a ver cómo calcular todos los divisores de un número. Esto nos puede servir, por ejemplo para calcular en cuantas piezas iguales podemos dividir un fotograma. Veamos un script que lo hace:
#!/bin/bash
function hallatodoslosdivisores()
{
divisores=""
for i in $(seq 1 $1)
do
let resto=$1%$i
if [ $resto -eq 0 ]
then
divisores=$divisores$i" "
fi
done
echo $divisores
}
let numero=1920
todoslosdivisores=$(hallatodoslosdivisores $numero)
echo "Los divisores de "$numero" son: "$todoslosdivisores
Explicación del script:
En una función llamada hallatodoslosdivisores, le indicamos que cree una variable vacía para poder ir rellenando de las cadenas que le digamos según si cumplen o no el requisito que buscamos. Después le decimos que recorra desde 1 hasta el número que le indicamos y que calcule el módulo, es decir, el resto de la división. Si el resto es 0, es que ese número es divisor del parámetro que le hemos indicado, así que se deberá incluir en el listado. En esa instrucción (divisores=$divisores$i" ") le digo que anexe también un espacio en blanco, así podemos separar fácilmente los divisores. Una vez recorrido desde 1 hasta el número indicado y hallados todos los divisores de ese número, que la función devuelva el resultado para poder trabajar con él.
Le indicamos un número. En este caso, 1920 por ser la anchura en píxeles del formato HD, pero le podemos indicar cualquier otro número. Para enviar un parámetro a una función y luego recibir en una variable el resultado de esa función escribimos:
variable=$(funcion $parametro)
Y, por último, comprobamos que el resultado es correcto.
¿Lo comprobamos? Aquí está:
Los divisores de 1920 son: 1 2 3 4 5 6 8 10 12 15 16 20 24 30 32 40 48 60 64 80 96 120 128 160 192 240 320 384 480 640 960 1920
Trabajar con funciones ahorra bastante tiempo y esfuerzo al incluir en las funciones tareas repetitivas, además de hacer scripts mucho más modulares y manejables, que nos facilita el mantenimiento y evolución de los scripts.
Ya hemos visto qué son las funciones en shell script, qué son los parámetros, tanto en shell script como en las funciones, cómo enviar parámetros a funciones y cómo recibir como variable el resultado de una función.
Ahora vamos a ver un sencillo truco para poder trabajar con funciones u otras porciones de código. Hagamos una función:
function enficheroexterno()
{
echo "Soy una función que está en distinto fichero que el script"
}
Y la guardo en un fichero que se llama funcionexterna (no hace falta que le pongamos extensión, aunque podemos hacerlo).
Y un script:
#!/bin/bash
. funcionexterna
enficheroexterno
Y cuando ejecuto el script, me da el siguiente resultado:
Soy una función que está en distinto fichero que el script
Para incluir la función de un fichero externo, únicamente he puesto un punto antes del nombre del fichero. De esa manera, incluye el texto del fichero en nuestro script.
Si en un shell script queremos comprobar cualquier cosa y, que el script actúe de manera distinta según el resultado de la comprobación usaremos:
if [ condición ]
then
# instrucciones a ejecutar si la condición es cierta
fi
O si queremos que en caso de que la condición sea falsa ejecute otras instrucciones:if [ condición ]
then
# instrucciones a ejecutar si la condición es cierta
else
# instrucciones a ejecutar en caso de que la condición sea falsa
fi
Y si queremos que vuelva a hacer una comprobación, en caso de que la condición sea cierta o falsa, usaremos:
if [ condición ]
then
# instrucciones a ejecutar si la condición es cierta
elif [ condición ]
then
# instrucciones a ejecutar si la segunda comprobación es cierta
else
# instrucciones a ejecutar si las dos comprobaciones anteriores son falsas
fi
Y podemos incluir tantos if dentro de otros if como queramos.
Para la temática que nos ocupa en este tutorial, comprobaremos:
Si un determinado directorio existe con [ -d $directorio ]
En caso de que queramos comprobar si el directorio no existe, se niega la condición con un signo de exclamación: [ ! -d $directorio ]
Si el directorio tiene algún fichero o directorio [ "$(ls $directorio)" ]
O si el directorio está vacío [ ! "$(ls $directorio)" ]
Vistas las aclaraciones, el código que comprueba si un directorio existe o no y que, en caso de que exista, compruebe si está vacío o no, sería:
if [ ! -d $directorio ]
then
echo "El directorio no existe"else
if [ "$(ls $directorio)" ]
then
echo "el directorio tiene algo" else
echo "el directorio está vacío" fi
fi
Tanto en los scrips en Bash como en sus funciones, los parámetros nos ofrece la posibilidad de interactuar con ellos, pudiéndole enviar datos para que los procese o que el script actúe de una determinada manera según los parámetros enviados. Veamos cómo trabajar con ellos y cómo sacarles partido.
Los parámetros se almacenan automáticamente en orden con un dólar delante del número que ocupan la posición que ocupan, empezando por 1, no por 0. Es decir, el primer parámetro de la función / script será $1; el segundo, $2... y así sucesivamente.
$0 se reserva para el nombre del script.
Ojo, en una función $0 es el nombre del script, pero $1 es el primer parámetro de la función, no del script.
#!/bin/bash
function saludo()
{
echo "Soy una función que se llama $0 y saluda: Hola, "$1
}
textosaludo=$(saludo Pepe)
echo $textosaludo
Si no le pasamos parámetros, devuelve:
./funcionquesaluda.sh
Soy una función que se llama ./funcionquesaluda.sh y saluda: Hola, Pepe
Y si le pasamos parámetros, devuelve lo mismo:
./funcionquesaluda.sh Manolo
Soy una función que se llama ./funcionquesaluda.sh y saluda: Hola, Pepe
Si queremos conocer todos los parámetros, usaremos $* y para saber el número de parámetros, $#.
Así pues, el script:
#!/bin/bash
function saludo()
{
echo "Soy una función que se llama" $0 "y saluda: Hola, "$1
echo "He recibido "$# " parámetros, que son: "$*
}
textosaludo=$(saludo Pepe María Juan Marcos Manolo Marta)
echo $textosaludo
Devuelve:
./funcionquesaluda.sh Manolo
Soy una función que se llama ./funcionquesaluda.sh y saluda: Hola, Pepe He recibido 6 parámetros, que son: Pepe María Juan Marcos Manolo Marta
Y aquí vemos un nuevo concepto. Al escribir varios echo en una función, concatena las cadenas. Si quisiéramos que no lo hiciera, añadiríamos un retorno de carro:
#!/bin/bash
function saludo()
{
echo "Soy una función que se llama" $0 "y saluda: Hola, "$1
echo -e "\r"
echo "He recibido "$# " parámetros, que son: "$*
}
textosaludo=$(saludo Pepe María Juan Marcos Manolo Marta)
echo $textosaludo
Nos devolvería:
./funcionquesaluda.sh Manolo
He recibido 6 parámetros, que son: Pepe María Juan Marcos Manolo Marta
Ya hemos visto cómo hacer funciones en shell script, cómo enviarle parámetros y ahora veremos cómo recoger en una variable el resultado del proceso de una función.
Veamos el ejemplo de los artículos anteriores modificado para recibir el resultado de la función en una variable:
#!/bin/bash
function saludo()
{
echo "Soy una función que saluda: Hola, "$1
}
textosaludo=$(saludo Pepe)
echo $textosalud
Como vemos, al igual que una función procesa los parámetros como un script ($1 es el primer parámetro recibido), a la hora de invocar la función y recibir el resultado en una variable también la instrucción es igual que si fuera un comando más de la shell:
variable=$(comando parametro1 parametro2...)
Definiendo la variable sin dólar delante. Recibiendo el valor con un igual, e indicándole que esa variable va a tomar el valor del resultado de la función con dólar y el comando/función con sus parámetros entre paréntesis.
Y no olvidemos que, aunque definamos las variables únicamente con el nombre, para trabar con el valor almacenado hay que escribir un dólar antes del nombre de la variable.