Jak przeglądać daty za pomocą Bash?


85

Mam taki skrypt basha:

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

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

Teraz chcę przeglądać zakres dat, np. 01.01.2015 do 31.01.2015.

Jak osiągnąć w Bash?

Aktualizacja :

Przyjemne do posiadania: żadne zadanie nie powinno być uruchamiane przed zakończeniem poprzedniego uruchomienia. W tym przypadku, po zakończeniu executeJobs.py bash $zwróci znak zachęty .

np. czy mogę włączyć wait%1do mojej pętli?


Czy jesteś na platformie z datą GNU?
Charles Duffy


1
Przy okazji, ponieważ masz pod ręką interpreter języka Python, byłoby to o wiele łatwiejsze do zrobienia w niezawodny i przenośny sposób przy użyciu datetimemodułu Python.
Charles Duffy

3
01.01.2015 do 31.01.2015 nie obejmuje dat dłuższych niż jeden miesiąc, więc jest to bardzo prosty przypadek.
Wintermute

2
... więc, jeśli rzeczywiście widząc zapotrzebowanie na wait(jak w, błędy zachodzące w wyniku procesów współbieżnych gdy nie robić), to trzeba coś bardziej interesującego / bardziej skomplikowane dzieje, którego potrzebuje bardziej skomplikowane rozwiązanie ( jak poproszenie podprocesu o odziedziczenie pliku blokującego), co jest na tyle skomplikowane i wystarczająco niezwiązane z arytmetyką datowania, że ​​powinno to być oddzielne pytanie.
Charles Duffy

Odpowiedzi:


197

Korzystanie z daty GNU:

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

Zauważ, że ponieważ używa to porównania ciągów, wymaga pełnej notacji ISO 8601 dla dat krawędzi (nie usuwaj wiodących zer). Aby sprawdzić poprawne dane wejściowe i przekształcić je w prawidłowy formularz, jeśli to możliwe, możesz również użyć 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

Ostatni dodatek : aby to sprawdzić $startdatewcześniej $enddate, jeśli spodziewasz się tylko dat z przedziału od 1000 do 9999, możesz po prostu użyć porównania ciągów w następujący sposób:

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

Aby być po bardzo bezpiecznej stronie po roku 10000, gdy porównanie leksykograficzne załamie się, użyj

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

Wyrażenie jest $(date -d "$d" +%Y%m%d)konwertowane $ddo postaci liczbowej, tj. 2015-02-23Staje się 20150223, a idea polega na tym, że daty w tej formie można porównywać liczbowo.


1
Jasne, czemu nie. To tylko pętla powłoki, która używa dat, ponieważ iterator nie zmienia tego, co możesz w niej zrobić.
Wintermute

1
@SirBenBenji, ... to powiedziawszy, %1jest konstrukcją kontroli zadań, a kontrola zadań jest wyłączona w nieinteraktywnych skryptach, chyba że wyraźnie ją włączysz. Właściwym sposobem odwoływania się do poszczególnych podprocesów wewnątrz skryptu jest PID, a nawet wtedy oczekiwanie na zakończenie procesów jest automatyczne, chyba że są one jawnie umieszczone w tle przez Twój kod (jak w przypadku a &) lub same się odłączają (w takim przypadku waitnawet nie zadziała, a PID podany powłoce zostanie unieważniony przez proces podwójnego rozwidlenia używany do samodzielnego tła).
Charles Duffy

1
Po bliższym przyjrzeniu się okazuje się, że sekundy przestępne są wykluczone ze znacznika czasu UNIX, tak że niektóre znaczniki czasu odnoszą się do przestrzeni dwóch sekund. To najwyraźniej sprawia, że ​​„gettimeofday” jest bardzo interesującym zastosowaniem w zakresie poniżej sekundy i przypuszczam, że powinniśmy liczyć się z tym, że sekundy przestępne nigdy nie zostały usunięte z roku. Oznacza to, że muszę się poprawić: dodanie 86400 sekund do znacznika czasu uniksowego jest prawdopodobnie zawsze to samo, co dodanie dnia, ponieważ nie ma sposobu, aby odnieść się konkretnie do 2016-12-31T23: 59: 60. TIL.
Wintermute

2
Uruchomienie twojego pierwszego kodu (sh test.sh) daje mi błąd: data: niedozwolona opcja - Używam: data [-jnRu] [-d dst] [-r sekundy] [-t zachód] [-v [+ | -] val [ymwdHMS]] ... [-f fmt data | [[[mm] dd] HH] MM [[cc] rr] [. ss]] [+ format]
dorien

2
W przypadku macOS to nie zadziała, najpierw zainstaluj datę GNU apple.stackexchange.com/questions/231224/ ...
Jaime Agudo

22

Rozszerzenie nawiasów :

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

Jeszcze:

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

Dowód:

$ 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

Kompaktowy / zagnieżdżony:

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

Zamówione, jeśli ma to znaczenie:

$ 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

Ponieważ jest nieuporządkowany, możesz po prostu ustawić lata przestępne na:

$ 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
A co z latami przestępnymi?
Wintermute

Czy mogę wtedy użyć python /home/user/executeJobs.py 2015-01-{01..31} &> /home/user/2015-01-{01..31}.log ?
Steve K

@SirBenBenji To zależy od executeJobs.py.
kojiro

Muszę powiedzieć, że executeJobs potrzebuje parametru daty i muszę czekać na zakończenie każdego uruchomienia. Jest to zadanie Big Data i w żadnym wypadku nie powinno się rozpoczynać przed zakończeniem każdego poprzedniego uruchomienia. Powinienem był o tym pomyśleć wcześniej, przepraszam, że o tym zapomniałem.
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

Miałem ten sam problem i wypróbowałem niektóre z powyższych odpowiedzi, może są w porządku, ale żadna z tych odpowiedzi nie rozwiązała tego, co próbowałem zrobić, używając macOS.

Próbowałem iterować daty w przeszłości i następujące rzeczy zadziałały dla mnie:

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

Mam nadzieję, że to pomoże.


Nie polecam używania nazwy zmiennej, która przypadkiem pokrywa się z bardzo potężnym poleceniem kopiowania bit po bicie, ale to tylko ja.
MerrillFraz,

Prostszym sposobem jest użycie gdatezamiast datemacOS.
northtree

2

Jeśli ktoś chce wykonać pętlę od daty wejściowej do dowolnego zakresu poniżej, może również wydrukować dane wyjściowe w formacie rrrrMMdd ...

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

Musiałem przeglądać daty w systemach AIX, BSD, Linux, OS X i Solaris. dateKomenda jest jedną z najmniej przenośnych i najbardziej nędznych komend do wykorzystania na różnych platformach, jakie napotykają. Łatwiej było napisać plikmy_date polecenie, które działało wszędzie.

Poniższy program w języku C przyjmuje datę początkową i dodaje lub odejmuje od niej dni. Jeśli nie podano daty, dodaje lub odejmuje dni od bieżącej daty.

my_dateKomenda pozwala na wykonywanie następujących wszędzie:

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

A kod 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 najlepiej pisać przy użyciu potoków (|). Powinno to skutkować wydajnym i współbieżnym (szybszym) przetwarzaniem pamięci. Napisałbym co następuje:

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

Poniżej zostanie wydrukowana data 20 aug 2020i data sprzed 100 dni.

Ten oneliner można przekształcić w narzędzie.

#!/usr/bin/env bash

# date-range template <template>

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

export LANG;

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

Domyślnie wybieramy iterację w ciągu ostatniego 1 dnia na raz.

$ 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

Powiedzmy, że chcemy wygenerować daty do określonej daty. Nie wiemy jeszcze, ile iteracji potrzebujemy, aby się tam dostać. Powiedzmy, że Tomek urodził się 1 stycznia 2001 roku. Chcemy wygenerować każdą datę do pewnej. Możemy to osiągnąć używając seda.

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

$((2**63-1))Sztuczka jest używany do tworzenia dużą liczbę całkowitą.

Po zakończeniu sed zamknie również narzędzie zakresu dat.

Można również iterować stosując interwał 3-miesięczny:

$ 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

Zrobiłem repozytorium z kolejnością dat, które ulepsza ten pomysł i dokumentuje go nieco lepiej. github.com/bas080/date-seq
bas080

1

Jeśli utkniesz z datą zajętości , uważam, że praca ze znacznikami czasu jest najbardziej niezawodnym podejściem:

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

Spowoduje to wyświetlenie:

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

Uniksowy znacznik czasu nie zawiera sekund przestępnych, więc 1 dzień to zawsze dokładnie 86400 sekund.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.