Как перебирать даты с помощью Bash?

87

У меня есть такой скрипт на bash:

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

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

Теперь я хочу перебрать диапазон дат, например с 01.01.2015 по 31.01.2015.

Как добиться в Баше?

Обновление :

Приятно иметь: никакие задания не должны начинаться до завершения предыдущего запуска. В этом случае, когда executeJobs.py завершится, $появится приглашение bash .

например, могу ли я включить wait%1в свой цикл?

Стив К
источник
Вы находитесь на платформе с датой GNU?
Чарльз Даффи
1
проверьте эту ссылку: glatter-gotz.com/blog/2011/02/19/…
qqibrow
1
Кстати, поскольку у вас есть под рукой интерпретатор Python, это было бы намного проще сделать надежным и переносимым способом с помощью datetimeмодуля Python.
Чарльз Даффи
3
2015-01-01 - 2015-01-31 не охватывает даты более одного месяца, так что это очень простой случай.
Wintermute
2
... так что , если вы на самом деле видите необходимость в wait(как, ошибки происходит из - за параллельные процессы , когда вы не), то у вас есть что - то более интересное / более осложненный происходит, что требует более сложного решения ( например, попросить подпроцесс унаследовать файл блокировки), что достаточно сложно и не имеет отношения к арифметике дат, поэтому это должен быть отдельный вопрос.
Чарльз Даффи

Ответы:

202

Используя дату 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

Обратите внимание, что, поскольку здесь используется сравнение строк, требуется полная запись крайних дат в соответствии с ISO 8601 (не удаляйте начальные нули). Чтобы проверить правильность входных данных и, если возможно, преобразовать их в допустимую форму, вы также можете использовать 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

И последнее дополнение : чтобы проверить, что $startdateэто было раньше $enddate, если вы ожидаете только даты между 1000 и 9999 годами, вы можете просто использовать сравнение строк следующим образом:

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

Чтобы обезопасить себя после 10000 года, когда лексикографическое сравнение не работает, используйте

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

Выражение $(date -d "$d" +%Y%m%d)преобразуется $dв числовую форму, т. Е. 2015-02-23Становится 20150223, и идея состоит в том, что даты в этой форме можно сравнивать численно.

Зимнее Безмолвие
источник
1
Конечно, почему бы и нет. Это просто цикл оболочки, который использует даты, поскольку итератор не меняет того, что вы можете делать внутри него.
Wintermute
1
@SirBenBenji, ... при этом, %1это конструкция управления заданиями, и управление заданиями отключено в неинтерактивных скриптах, если вы явно не включите его сами. Правильный способ ссылки на отдельные подпроцессы внутри сценария - это PID, и даже в этом случае ожидание завершения процессов происходит автоматически, если только они явно не основаны на вашем коде (как с a &) или не отключаются самостоятельно (в этом случае waitдаже не будет работать, и PID, предоставленный оболочке, будет аннулирован процессом двойной вилки, используемым для самофонового режима).
Чарльз Даффи
1
При внимательном рассмотрении выясняется, что дополнительные секунды исключены из метки времени UNIX, поэтому некоторые метки времени относятся к интервалу в две секунды. Это, очевидно, делает «gettimeofday» очень интересным для реализации в субсекундном диапазоне, и я полагаю, мы должны считать себя удачливыми, что високосные секунды никогда не удалялись из года. Это означает, что я должен исправить себя: добавление 86400 секунд к метке времени unix, возможно, всегда то же самое, что добавление дня, поскольку нет возможности конкретно сослаться на 2016-12-31T23: 59: 60. TIL.
Wintermute
2
Запуск вашего первого кода (sh test.sh) дает мне сообщение об ошибке: дата: недопустимая опция - Я использую: date [-jnRu] [-d dst] [-r секунды] [-t west] [-v [+ | -] val [ymwdHMS]] ... [-f fmt date | [[[mm] dd] HH] MM [[cc] yy] [. ss]] [+ format]
dorien
3
Для macOS это не сработает, сначала установите gnu date apple.stackexchange.com/questions/231224/…
Хайме Агудо,
22

Расширение скобок :

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

Больше:

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

Доказательство:

$ 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

Компактный / вложенный:

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

Заказал, если важно:

$ 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

Поскольку он неупорядочен, вы можете просто добавить високосные годы:

$ 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
А как насчет високосных лет?
Wintermute
Могу я тогда использовать python /home/user/executeJobs.py 2015-01-{01..31} &> /home/user/2015-01-{01..31}.log ?
Стив К.
@SirBenBenji Это зависит от executeJobs.py.
kojiro
Я должен сказать, что executeJobs нуждается в параметре даты, и мне нужно ждать завершения каждого запуска. Это работа с большими данными, и ее ни в коем случае нельзя запускать до завершения каждого предыдущего запуска. Я должен был подумать об этом раньше, извините, что забыл об этом.
Стив К.
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

У меня была такая же проблема, и я попробовал некоторые из приведенных выше ответов, возможно, они в порядке, но ни один из этих ответов не зафиксировал то, что я пытался сделать, используя macOS.

Я пытался перебирать даты в прошлом, и у меня сработало следующее:

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

Надеюсь, поможет.

Abranhe
источник
Я рекомендую не использовать имя переменной, которое совпадает с очень мощной командой побитового копирования, но это только я.
MerrillFraz 03
Более простой способ - использовать gdateвместо datemacOS.
northtree
2

Если кто-то хочет перейти от даты ввода к любому диапазону, указанному ниже, он также будет печатать вывод в формате yyyyMMdd ...

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

Мне нужно было просмотреть даты в AIX, BSD, Linux, OS X и Solaris. Эта dateкоманда - одна из наименее переносимых и самых жалких команд для использования на разных платформах, с которыми я сталкивался. Мне было легче написатьmy_date команду, которая просто работала везде.

Программа C ниже берет дату начала и добавляет или вычитает дни из нее. Если дата не указана, она добавляет или вычитает дни из текущей даты.

Команда my_dateпозволяет везде выполнять следующее:

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

И код C:

jww
источник
2

Bash лучше всего писать, используя каналы (|). Это должно привести к эффективному использованию памяти и одновременной (более быстрой) обработке. Я бы написал следующее:

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

Ниже будет напечатана дата 20 aug 2020 и распечатает даты за 100 дней до нее.

Этот одиночный лайнер можно превратить в служебную программу.

#!/usr/bin/env bash

# date-range template <template>

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

export LANG;

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

По умолчанию мы выбираем итерацию в последний день за раз.

$ 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

Допустим, мы хотим сгенерировать даты до определенной даты. Мы еще не знаем, сколько итераций нам нужно для этого. Допустим, Том родился 1 января 2001 года. Мы хотим генерировать каждую дату до определенной. Этого можно добиться с помощью sed.

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

$((2**63-1))Прием используется для создания большого целого числа.

После выхода sed он также выйдет из утилиты диапазона дат.

Можно также выполнить итерацию с интервалом в 3 месяца:

$ 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
источник
Я сделал репозиторий date-seq, который улучшает эту идею и немного лучше ее документирует. github.com/bas080/date-seq
bas080
1

Если вы застряли с датой busybox , я считаю, что работа с метками времени является наиболее надежным подходом:

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

Это выведет:

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

Метка времени Unix не включает дополнительные секунды, поэтому 1 день всегда равен 86400 секундам.

Геллвейлер
источник