¿Cómo recorrer las fechas usando Bash?


85

Tengo tal script bash:

array=( '2015-01-01', '2015-01-02' )

for i in "${array[@]}"
do
    python /home/user/executeJobs.py {i} &> /home/user/${i}.log
done

Ahora quiero recorrer un rango de fechas, por ejemplo, 2015-01-01 hasta 2015-01-31.

¿Cómo lograrlo en Bash?

Actualización :

Es bueno tenerlo: ningún trabajo debe iniciarse antes de que se haya completado una ejecución anterior. En este caso, cuando se complete executeJobs.py, el indicador de bash $volverá.

por ejemplo, ¿podría incorporar wait%1en mi bucle?


¿Estás en una plataforma con fecha GNU?
Charles Duffy


1
Por cierto, dado que tiene un intérprete de Python a mano, esto sería mucho, mucho más fácil de hacer de una manera confiable y portátil usando el datetimemódulo de Python.
Charles Duffy

3
2015-01-01 hasta 2015-01-31 no abarca fechas en más de un mes, por lo que es un caso muy simple.
Wintermute

2
... así que, si en realidad estás viendo una necesidad que wait(como en, errores que suceden debido a los procesos concurrentes cuando no lo hace), entonces usted tiene algo más interesante / complicado ir más adelante, que necesita una solución más complicada ( como pedirle al subproceso que herede un archivo de bloqueo), que es lo suficientemente complejo y no está lo suficientemente relacionado con la aritmética de la fecha como para que sea una pregunta separada.
Charles Duffy

Respuestas:


197

Usando la fecha GNU:

d=2015-01-01
while [ "$d" != 2015-02-20 ]; do 
  echo $d
  d=$(date -I -d "$d + 1 day")
done

Tenga en cuenta que debido a que utiliza la comparación de cadenas, requiere la notación ISO 8601 completa de las fechas del borde (no elimine los ceros iniciales). Para verificar datos de entrada válidos y convertirlos en un formulario válido si es posible, también puede usar date:

# slightly malformed input data
input_start=2015-1-1
input_end=2015-2-23

# After this, startdate and enddate will be valid ISO 8601 dates,
# or the script will have aborted when it encountered unparseable data
# such as input_end=abcd
startdate=$(date -I -d "$input_start") || exit -1
enddate=$(date -I -d "$input_end")     || exit -1

d="$startdate"
while [ "$d" != "$enddate" ]; do 
  echo $d
  d=$(date -I -d "$d + 1 day")
done

Una última adición : para verificar eso $startdatees antes $enddate, si solo espera fechas entre los años 1000 y 9999, simplemente puede usar una comparación de cadenas como esta:

while [[ "$d" < "$enddate" ]]; do

Para estar muy seguro más allá del año 10000, cuando se rompa la comparación lexicográfica, utilice

while [ "$(date -d "$d" +%Y%m%d)" -lt "$(date -d "$enddate" +%Y%m%d)" ]; do

La expresión se $(date -d "$d" +%Y%m%d)convierte $da una forma numérica, es decir, se 2015-02-23convierte 20150223, y la idea es que las fechas en esta forma se pueden comparar numéricamente.


1
Seguro Por qué no. Es solo un bucle de shell, que usa fechas como iterador no cambia lo que puede hacer dentro de él.
Wintermute

1
@SirBenBenji, ... dicho esto, %1es una construcción de control de trabajos, y el control de trabajos está desactivado en scripts no interactivos a menos que lo active explícitamente usted mismo. La forma correcta de referirse a subprocesos individuales dentro de un script es mediante PID, e incluso entonces, esperar a que los procesos se completen es automático, a menos que estén explícitamente respaldados por su código (como con a &), o se separen automáticamente (en cuyo caso waitni siquiera funcionará, y el PID dado al shell será invalidado por el proceso de doble bifurcación utilizado para el auto-fondo).
Charles Duffy

1
Después de mirar más de cerca, parece que los segundos intercalares están excluidos de la marca de tiempo de UNIX, por lo que algunas marcas de tiempo se refieren a un espacio de dos segundos. Esto aparentemente hace que ´gettimeofday` sea muy interesante de implementar en el rango de menos de un segundo, y supongo que debemos considerarnos afortunados de que los segundos intercalares nunca se hayan eliminado de un año. Esto significa que tengo que corregirme: agregar 86400 segundos a una marca de tiempo de Unix es posiblemente siempre lo mismo que agregar un día, ya que no hay forma de referirse específicamente a 2016-12-31T23: 59: 60. TIL.
Wintermute

2
Ejecutar su primer código (sh test.sh) me da un error: fecha: opción ilegal - uso: fecha [-jnRu] [-d dst] [-r segundos] [-t oeste] [-v [+ | -] val [ymwdHMS]] ... [-f fmt fecha | [[[mm] dd] HH] MM [[cc] aa] [. ss]] [+ formato]
dorien

2
Para macOS no funcionará, primero instale gnu date apple.stackexchange.com/questions/231224/…
Jaime Agudo

22

Expansión de la abrazadera :

for i in 2015-01-{01..31} …

Más:

for i in 2015-02-{01..28} 2015-{04,06,09,11}-{01..30} 2015-{01,03,05,07,08,10,12}-{01..31} …

Prueba:

$ echo 2015-02-{01..28} 2015-{04,06,09,11}-{01..30} 2015-{01,03,05,07,08,10,12}-{01..31} | wc -w
 365

Compacto / anidado:

$ echo 2015-{02-{01..28},{04,06,09,11}-{01..30},{01,03,05,07,08,10,12}-{01..31}} | wc -w
 365

Ordenado, si importa:

$ x=( $(printf '%s\n' 2015-{02-{01..28},{04,06,09,11}-{01..30},{01,03,05,07,08,10,12}-{01..31}} | sort) )
$ echo "${#x[@]}"
365

Dado que no está ordenado, puede seguir los años bisiestos:

$ echo {2015..2030}-{02-{01..28},{04,06,09,11}-{01..30},{01,03,05,07,08,10,12}-{01..31}} {2016..2028..4}-02-29 | wc -w
5844

3
¿Qué pasa con los años bisiestos?
Wintermute

¿Puedo usar entonces python /home/user/executeJobs.py 2015-01-{01..31} &> /home/user/2015-01-{01..31}.log ?
Steve K

@SirBenBenji Eso depende de executeJobs.py.
kojiro

Debo decir que executeJobs necesita el parámetro de fecha y necesito esperar a que se complete cada ejecución. Es un trabajo de Big Data y bajo ninguna circunstancia debe iniciarse antes de que se complete cada ejecución anterior. Debería haber pensado en esto antes, lamento haberlo olvidado.
Steve K

10
start='2019-01-01'
end='2019-02-01'

start=$(date -d $start +%Y%m%d)
end=$(date -d $end +%Y%m%d)

while [[ $start -le $end ]]
do
        echo $start
        start=$(date -d"$start + 1 day" +"%Y%m%d")

done

3

Tuve el mismo problema y probé algunas de las respuestas anteriores, tal vez estén bien, pero ninguna de esas respuestas se solucionó en lo que estaba tratando de hacer, usando macOS.

Estaba tratando de iterar sobre fechas en el pasado, y lo siguiente es lo que funcionó para mí:

#!/bin/bash

# Get the machine date
newDate=$(date '+%m-%d-%y')

# Set a counter variable
counter=1 

# Increase the counter to get back in time
while [ "$newDate" != 06-01-18 ]; do
  echo $newDate
  newDate=$(date -v -${counter}d '+%m-%d-%y')
  counter=$((counter + 1))
done

Espero eso ayude.


Recomiendo no usar un nombre de variable que coincida con un comando de copia bit a bit muy poderoso, pero ese soy solo yo.
MerrillFraz

La forma más sencilla es usarlo en gdatelugar de dateen macOS.
northtree

2

Si uno quiere recorrer desde la fecha de entrada hasta cualquier rango a continuación, se puede usar, también imprimirá la salida en formato de aaaaMMdd ...

#!/bin/bash
in=2018-01-15
while [ "$in" != 2018-01-25 ]; do
  in=$(date -I -d "$in + 1 day")
  x=$(date -d "$in" +%Y%m%d)
  echo $x
done

2

Necesitaba recorrer las fechas en AIX, BSD, Linux, OS X y Solaris. El datecomando es uno de los comandos menos portátiles y más miserables para usar en todas las plataformas que he encontrado. Me resultó más fácil escribir un my_datecomando que simplemente funcionaba en todas partes.

El programa C a continuación toma una fecha de inicio y agrega o resta días de ella. Si no se proporciona una fecha, suma o resta días de la fecha actual.

El my_datecomando le permite realizar lo siguiente en todas partes:

start="2015-01-01"
stop="2015-01-31"

echo "Iterating dates from ${start} to ${stop}."

while [[ "${start}" != "${stop}" ]]
do
    python /home/user/executeJobs.py {i} &> "/home/user/${start}.log"
    start=$(my_date -s "${start}" -n +1)
done

Y el código C:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>

int show_help();

int main(int argc, char* argv[])
{
    int eol = 0, help = 0, n_days = 0;
    int ret = EXIT_FAILURE;

    time_t startDate = time(NULL);
    const time_t ONE_DAY = 24 * 60 * 60;

    for (int i=0; i<argc; i++)
    {
        if (strcmp(argv[i], "-l") == 0)
        {
            eol = 1;
        }
        else if (strcmp(argv[i], "-n") == 0)
        {
            if (++i == argc)
            {
                show_help();
                ret = EXIT_FAILURE;
                goto finish;
            }

            n_days = strtoll(argv[i], NULL, 0);
        }
        else if (strcmp(argv[i], "-s") == 0)
        {
            if (++i == argc)
            {
                show_help();
                ret = EXIT_FAILURE;
                goto finish;
            }

            struct tm dateTime;
            memset (&dateTime, 0x00, sizeof(dateTime));

            const char* start = argv[i];
            const char* end = strptime (start, "%Y-%m-%d", &dateTime);

            /* Ensure all characters are consumed */
            if (end - start != 10)
            {
                show_help();
                ret = EXIT_FAILURE;
                goto finish;
            }

            startDate = mktime (&dateTime);
        }
    }

    if (help == 1)
    {
        show_help();
        ret = EXIT_SUCCESS;
        goto finish;
    }

    char buff[32];
    const time_t next = startDate + ONE_DAY * n_days;
    strftime(buff, sizeof(buff), "%Y-%m-%d", localtime(&next));

    /* Paydirt */
    if (eol)
        fprintf(stdout, "%s\n", buff);
    else
        fprintf(stdout, "%s", buff);

    ret = EXIT_SUCCESS;

finish:

    return ret;
}

int show_help()
{
    fprintf(stderr, "Usage:\n");
    fprintf(stderr, "  my_date [-s date] [-n [+|-]days] [-l]\n");
    fprintf(stderr, "    -s date: optional, starting date in YYYY-MM-DD format\n");
    fprintf(stderr, "    -n days: optional, number of days to add or subtract\n");
    fprintf(stderr, "    -l: optional, add new-line to output\n");
    fprintf(stderr, "\n");
    fprintf(stderr, "  If no options are supplied, then today is printed.\n");
    fprintf(stderr, "\n");
    return 0;
}

2

Bash se escribe mejor aprovechando las tuberías (|). Esto debería resultar en un procesamiento concurrente (más rápido) y eficiente en la memoria. Escribiría lo siguiente:

seq 0 100 | xargs printf "20 Aug 2020 - %sdays\n" \
  | xargs -d '\n' -l date -d

Lo siguiente imprimirá la fecha 20 aug 2020y las fechas de los 100 días anteriores.

Este delineador puede convertirse en una utilidad.

#!/usr/bin/env bash

# date-range template <template>

template="${1:--%sdays}"

export LANG;

xargs printf "$template\n" | xargs -d '\n' -l date -d

De forma predeterminada, elegimos iterar en el último día a la vez.

$ seq 10 | date-range
Mon Mar  2 17:42:43 CET 2020
Sun Mar  1 17:42:43 CET 2020
Sat Feb 29 17:42:43 CET 2020
Fri Feb 28 17:42:43 CET 2020
Thu Feb 27 17:42:43 CET 2020
Wed Feb 26 17:42:43 CET 2020
Tue Feb 25 17:42:43 CET 2020
Mon Feb 24 17:42:43 CET 2020
Sun Feb 23 17:42:43 CET 2020
Sat Feb 22 17:42:43 CET 2020

Digamos que queremos generar fechas hasta una fecha determinada. Aún no sabemos cuántas iteraciones necesitamos para llegar allí. Digamos que Tom nació el 1 de enero de 2001. Queremos generar cada fecha hasta una determinada. Podemos lograr esto usando sed.

seq 0 $((2**63-1)) | date-range | sed '/.. Jan 2001 /q'

El $((2**63-1))truco se usa para crear un número entero grande.

Una vez que sed sale, también saldrá de la utilidad de rango de fechas.

También se puede iterar usando un intervalo de 3 meses:

$ seq 0 3 12 | date-range '+%smonths'
Tue Mar  3 18:17:17 CET 2020
Wed Jun  3 19:17:17 CEST 2020
Thu Sep  3 19:17:17 CEST 2020
Thu Dec  3 18:17:17 CET 2020
Wed Mar  3 18:17:17 CET 2021

Hice un repositorio de date-seq que mejora esta idea y la documenta un poco mejor. github.com/bas080/date-seq
bas080

1

Si está atascado con la fecha de busybox , he descubierto que trabajar con marcas de tiempo es el enfoque más confiable:

STARTDATE="2019-12-30"
ENDDATE="2020-01-04"

start=$(date -d $STARTDATE +%s)
end=$(date -d $ENDDATE +%s)

d="$start"
while [[ $d -le $end ]]
do
    date -d @$d +%Y-%m-%d

    d=$(( $d + 86400 ))
done

Esto dará como resultado:

2019-12-30
2019-12-31
2020-01-01
2020-01-02
2020-01-03
2020-01-04

La marca de tiempo de Unix no incluye segundos intercalares, por lo que 1 día es siempre exactamente 86400 segundos.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.