Como fazer um loop em datas usando o Bash?

87

Eu tenho esse 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

Agora, quero percorrer um intervalo de datas, por exemplo, 01-01-2015 até 31/01/2015.

Como conseguir no Bash?

Atualização :

Agradável: nenhum trabalho deve ser iniciado antes da conclusão de uma execução anterior. Nesse caso, quando executeJobs.py for concluído, o prompt do bash $retornará.

por exemplo, eu poderia incorporar wait%1no meu loop?

Steve K
fonte
Você está em uma plataforma com data GNU?
Charles Duffy
1
verifique este link: glatter-gotz.com/blog/2011/02/19/…
qqibrow
1
A propósito, como você tem um interpretador Python à mão, isso seria muito, muito mais fácil de fazer de forma confiável e portátil usando o datetimemódulo Python.
Charles Duffy
3
2015-01-01 até 2015-01-31 não abrange datas em mais de um mês, então é um caso muito simples.
Wintermute de
2
... então, se você está realmente vendo a necessidade de wait(como em bugs acontecendo devido a processos simultâneos quando você não), então você tem algo mais interessante / mais complicado acontecendo, que precisa de uma solução mais complicada ( como pedir ao subprocesso para herdar um arquivo de bloqueio), o que é suficientemente complexo e não relacionado com a aritmética de data para que seja uma questão separada.
Charles Duffy

Respostas:

202

Usando a data GNU:

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

  # mac option for d decl (the +1d is equivalent to + 1 day)
  # d=$(date -j -v +1d -f "%Y-%m-%d" "2020-12-12" +%Y-%m-%d)
done

Observe que, por usar comparação de strings, é necessária a notação ISO 8601 completa das datas das bordas (não remova os zeros à esquerda). Para verificar se há dados de entrada válidos e forçá-los a um formato válido, se possível, você também pode 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

Uma adição final : para verificar se $startdateé antes $enddate, se você espera apenas datas entre os anos 1000 e 9999, pode simplesmente usar a comparação de strings como esta:

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

Para estar no lado muito seguro além do ano 10.000, quando a comparação lexicográfica falhar, use

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

A expressão é $(date -d "$d" +%Y%m%d)convertida $dpara a forma numérica, ou seja, 2015-02-23torna-se 20150223, e a ideia é que as datas nessa forma possam ser comparadas numericamente.

Wintermute
fonte
1
Claro, por que não. É apenas um loop de shell, que usa datas como iterador, não altera o que você pode fazer dentro dele.
Wintermute de
1
@SirBenBenji, ... dito isso, %1é uma construção de controle de trabalho, e o controle de trabalho é desativado em scripts não interativos, a menos que você o ligue explicitamente. A maneira adequada de se referir a subprocessos individuais dentro de um script é por PID e, mesmo assim, esperar que os processos sejam concluídos é automático, a menos que estejam explicitamente em segundo plano por seu código (como com um &), ou se auto-separem (nesse caso waitnem funcionará, e o PID fornecido ao shell será invalidado pelo processo de bifurcação dupla usado para o fundo próprio).
Charles Duffy
1
Depois de dar uma olhada mais de perto, parece que os segundos intercalados foram excluídos do carimbo de data / hora UNIX, de forma que alguns carimbos de data / hora se referem a um espaço de dois segundos. Isso aparentemente torna ´gettimeofday` muito interessante de implementar na faixa de sub-segundos, e suponho que devemos nos considerar com sorte porque os segundos bissextos nunca foram removidos de um ano. Isso significa que tenho que me corrigir: adicionar 86400 segundos a um carimbo de data / hora Unix é sem dúvida sempre o mesmo que adicionar um dia, uma vez que não há como se referir a 2016-12-31T23: 59: 60 especificamente. TIL.
Wintermute
2
Executar seu primeiro código (sh test.sh) me dá um erro: data: opção ilegal - I uso: data [-jnRu] [-d dst] [-r segundos] [-t oeste] [-v [+ | -] val [ymwdHMS]] ... [-f fmt date | [[[mm] dd] HH] MM [[cc] aa] [. ss]] [+ formato]
dorien
3
Para macOS não funcionará, primeiro instale gnu date apple.stackexchange.com/questions/231224/…
Jaime Agudo
22

Expansão da cinta :

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

Mais:

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

Prova:

$ 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 / aninhado:

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

Solicitado, se for importante:

$ 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

Como não está ordenado, você pode simplesmente adicionar anos bissextos a:

$ 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
kojiro
fonte
3
E anos bissextos?
Wintermute de
Posso então usar python /home/user/executeJobs.py 2015-01-{01..31} &> /home/user/2015-01-{01..31}.log ?
Steve K
@SirBenBenji Isso depende executeJobs.py.
kojiro
Devo dizer que executeJobs precisa do parâmetro de data e preciso esperar a conclusão de cada execução. É um trabalho de Big Data e não deve ser iniciado em nenhuma circunstância antes da conclusão de cada execução anterior. Eu deveria ter pensado nisso antes, desculpe por ter esquecido.
Steve K
11
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
Gilli
fonte
3

Eu tive o mesmo problema e tentei algumas das respostas acima, talvez estejam ok, mas nenhuma dessas respostas corrigiu o que eu estava tentando fazer, usando o macOS.

Eu estava tentando iterar datas no passado, e o seguinte funcionou para mim:

#!/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 que ajude.

abranhe
fonte
Eu recomendo não usar um nome de variável que coincida com um comando de cópia bit a bit muito poderoso, mas isso é só comigo.
MerrillFraz
A maneira mais simples é usar em gdatevez do datemacOS.
Northtree
2

Se alguém quiser fazer um loop da data de entrada para qualquer intervalo abaixo pode ser usado, ele também imprimirá a saída no 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
ankitbaldua
fonte
2

Eu precisava fazer um loop através de datas no AIX, BSDs, Linux, OS X e Solaris. O datecomando é um dos comandos menos portáteis e mais miseráveis ​​para usar em todas as plataformas que encontrei. Achei mais fácil escrever um my_datecomando que funcionasse em qualquer lugar.

O programa C abaixo pega uma data de início e adiciona ou subtrai dias dela. Se nenhuma data for fornecida, ele adiciona ou subtrai dias da data atual.

O my_datecomando permite que você execute o seguinte em qualquer lugar:

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

E o código C:

jww
fonte
2

O Bash é melhor escrito utilizando tubos (|). Isso deve resultar em processamento eficiente da memória e simultâneo (mais rápido). Eu escreveria o seguinte:

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

O seguinte imprimirá a data 20 aug 2020e imprimirá as datas dos 100 dias anteriores.

Este oneliner pode ser transformado em um utilitário.

#!/usr/bin/env bash

# date-range template <template>

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

export LANG;

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

Por padrão, optamos por iterar no último dia 1 por 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 desejamos gerar datas até uma determinada data. Não sabemos ainda quantas iterações precisamos para chegar lá. Digamos que Tom nasceu em 1 de janeiro de 2001. Queremos gerar cada data até uma determinada. Podemos conseguir isso usando sed.

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

O $((2**63-1))truque é usado para criar um grande número inteiro.

Assim que o sed sair, ele também sairá do utilitário de intervalo de datas.

Também é possível iterar usando um 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
bas080
fonte
Fiz um repositório date-seq que aprimora essa ideia e a documenta um pouco melhor. github.com/bas080/date-seq
bas080
1

Se você está preso à data do busybox , descobri que trabalhar com timestamps é a abordagem mais confiável:

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

Isso resultará em:

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

O carimbo de data / hora Unix não inclui segundos bissextos, então 1 dia é sempre igual a exatamente 86400 segundos.

Gellweiler
fonte