Come scorrere le date usando Bash?


87

Ho un tale 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

Ora voglio scorrere un intervallo di date, ad esempio 2015-01-01 fino al 2015-01-31.

Come ottenere in Bash?

Aggiornamento :

Bello da avere: nessun lavoro dovrebbe essere avviato prima del completamento di un'esecuzione precedente. In questo caso, quando executeJobs.py è completato, il prompt di bash $tornerà.

ad es. potrei incorporare wait%1nel mio ciclo?


Sei su una piattaforma con data GNU?
Charles Duffy

1

1
A proposito, dal momento che hai un interprete Python a portata di mano, questo sarebbe molto, molto più facile da fare in modo affidabile e portatile usando il datetimemodulo Python.
Charles Duffy

3
2015-01-01 fino al 2015-01-31 non copre le date in più di un mese, quindi è un caso molto semplice.
Wintermute

2
... quindi, se stai effettivamente vedendo la necessità di wait(come in, bug che si verificano a causa di processi simultanei quando non lo fai), allora hai qualcosa di più interessante / più complicato in corso, che necessita di una soluzione più complicata ( come chiedere al sottoprocesso di ereditare un file di blocco), che è abbastanza complessa e sufficientemente non correlata alla data aritmetica che dovrebbe essere una domanda separata.
Charles Duffy

Risposte:


202

Utilizzando la 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

Si noti che poiché utilizza il confronto tra stringhe, è necessaria la notazione ISO 8601 completa delle date limite (non rimuovere gli zeri iniziali). Per verificare la presenza di dati di input validi e costringerli a un modulo valido, se possibile, puoi utilizzare dateanche:

# 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

Un'ultima aggiunta : per verificare che $startdatesia prima $enddate, se prevedi solo date comprese tra gli anni 1000 e 9999, puoi semplicemente utilizzare il confronto tra stringhe in questo modo:

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

Per essere al sicuro oltre l'anno 10000, quando il confronto lessicografico si interrompe, usa

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

L'espressione si $(date -d "$d" +%Y%m%d)converte $din una forma numerica, cioè 2015-02-23diventa 20150223, e l'idea è che le date in questa forma possano essere confrontate numericamente.


1
Certo, perché no. È solo un ciclo di shell, che utilizza le date poiché l'iteratore non cambia ciò che puoi fare al suo interno.
Wintermute

1
@SirBenBenji, ... detto questo, %1è un costrutto di controllo del lavoro e il controllo del lavoro è disattivato negli script non interattivi a meno che tu non lo attivi esplicitamente. Il modo corretto per fare riferimento a singoli sottoprocessi all'interno di uno script è tramite PID, e anche in questo caso l'attesa del completamento dei processi è automatica a meno che non siano esplicitamente inseriti in background dal codice (come con a &), o si auto-scollegano (nel qual caso waitnon funzionerà nemmeno e il PID assegnato alla shell verrà invalidato dal processo a doppio fork utilizzato per l'auto-background).
Charles Duffy

1
Dopo aver esaminato più da vicino, sembra che i secondi intercalari siano esclusi dal timestamp UNIX, in modo che alcuni timestamp si riferiscano a uno spazio di due secondi. Apparentemente questo rende ´gettimeofday` molto interessante da implementare nella fascia dei secondi, e suppongo che dovremmo ritenerci fortunati che i secondi intercalari non siano mai stati rimossi da un anno. Ciò significa che devo correggermi: aggiungere 86400 secondi a un timestamp unix è probabilmente sempre uguale all'aggiunta di un giorno, poiché non è possibile fare riferimento specificamente a 2016-12-31T23: 59: 60. TIL.
Wintermute

2
L'esecuzione del primo codice (sh test.sh) mi dà un errore: data: opzione illegale - Uso: data [-jnRu] [-d dst] [-r secondi] [-t ovest] [-v [+ | -] val [ymwdHMS]] ... [-f fmt date | [[[mm] gg] HH] MM [[cc] aa] [. ss]] [+ formato]
dorien

3
Per macOS non funzionerà, installa prima gnu date apple.stackexchange.com/questions/231224/…
Jaime Agudo

22

Espansione del tutore :

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

Di Più:

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

Compatto / annidato:

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

Ordinato, se è 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

Dal momento che non è ordinato, puoi semplicemente aggiungere anni bisestili:

$ 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
E gli anni bisestili?
Wintermute

Posso quindi usare python /home/user/executeJobs.py 2015-01-{01..31} &> /home/user/2015-01-{01..31}.log ?
Steve K

@SirBenBenji Dipende da executeJobs.py.
kojiro

Devo dire che executeJobs ha bisogno del parametro date e devo aspettare che ogni esecuzione venga completata. È un lavoro di Big Data e non deve in alcun caso essere avviato prima del completamento di ciascuna esecuzione precedente. Avrei dovuto pensarci prima, scusa se me ne sono dimenticato.
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

3

Ho avuto lo stesso problema e ho provato alcune delle risposte sopra, forse sono ok, ma nessuna di queste risposte ha risolto quello che stavo cercando di fare, usando macOS.

Stavo cercando di ripetere le date in passato, e quanto segue è ciò che ha funzionato per me:

#!/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

Spero che sia d'aiuto.


Raccomando di non utilizzare un nome di variabile che coincida con un comando di copia bit per bit molto potente, ma sono solo io.
MerrillFraz

Il modo più semplice è usare gdateinvece che datein macOS.
northtree

2

Se si desidera eseguire il ciclo dalla data di input a qualsiasi intervallo di seguito può essere utilizzato, verrà anche stampato l'output nel formato aaaaMMgg ...

#!/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

Avevo bisogno di scorrere le date su AIX, BSD, Linux, OS X e Solaris. Il datecomando è uno dei comandi meno portabili e più miserabili da utilizzare sulle piattaforme che ho incontrato. Ho trovato più facile scrivere un my_datecomando che funzionasse ovunque.

Il programma C seguente prende una data di inizio e aggiunge o sottrae giorni da essa. Se non viene fornita alcuna data, aggiunge o sottrae giorni dalla data corrente.

Il my_datecomando ti consente di eseguire le seguenti operazioni ovunque:

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 il codice C:


2

Bash è meglio scritto sfruttando le pipe (|). Ciò dovrebbe comportare un'elaborazione efficiente della memoria e simultanea (più veloce). Vorrei scrivere quanto segue:

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

Quanto segue stamperà la data 20 aug 2020e stamperà le date dei 100 giorni precedenti.

Questo oneliner può essere trasformato in un'utilità.

#!/usr/bin/env bash

# date-range template <template>

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

export LANG;

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

Per impostazione predefinita, scegliamo di iterare negli ultimi 1 giorno alla volta.

$ 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

Supponiamo di voler generare date fino a una certa data. Non sappiamo ancora quante iterazioni abbiamo bisogno per arrivarci. Diciamo che Tom è nato il 1 gennaio 2001. Vogliamo generare ogni data fino a una certa. Possiamo ottenere ciò utilizzando sed.

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

Il $((2**63-1))trucco viene utilizzato per creare un numero intero grande.

Una volta che sed esce, uscirà anche dall'utilità di intervallo di date.

Si può anche iterare utilizzando un intervallo di 3 mesi:

$ 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

Ho creato un repository date-seq che migliora questa idea e la documenta un po 'meglio. github.com/bas080/date-seq
bas080

1

Se sei bloccato con la data busybox , ho trovato che lavorare con i timestamp è l'approccio più affidabile:

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

Questo produrrà:

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

Il timestamp Unix non include i secondi intercalari, quindi 1 giorno equivale sempre esattamente a 86400 secondi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.