如何使用Bash循环遍历日期?

127

我有这样的Bash脚本:

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

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

现在我想要循环一系列日期,比如从2015-01-01到2015-01-31。

如何在Bash中实现?

更新:

附加需求:在上一次运行完成前不应启动任何作业。在这种情况下,当executeJobs.py完成后,bash提示符$将返回。

例如,我能否在我的循环中并入wait%1


1
请查看此链接:http://www.glatter-gotz.com/blog/2011/02/19/looping-through-dates-in-a-bash-script-on-osx/ - qqibrow
1
顺便说一句,既然你有一个Python解释器,使用datetime Python模块可以更加可靠和便携地完成这个任务。 - Charles Duffy
3
2015年1月1日至2015年1月31日期间不跨越多个月份,因此这是一个非常简单的情况。 - Wintermute
2
如果你真的看到了需要“等待”的情况(比如,由于并发进程而导致错误),那么你可能遇到了更有趣/更复杂的问题,需要更复杂的解决方案(比如要求子进程继承一个锁文件),这已经足够复杂且与日期算术无关,应该是一个单独的问题。 - Charles Duffy
这个 Python 脚本调用了其他的 Bash 脚本,具体来说是 mrjob 和文件系统操作以及 pymongo。 - Stephan Kristyn
显示剩余12条评论
10个回答

275

使用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" $d +%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
当然可以。这只是一个shell循环,使用日期作为迭代器并不会改变你在其中所能做的事情。 - Wintermute
1
@SirBenBenji,...话说,%1是一个作业控制结构,在非交互式脚本中作业控制被关闭,除非你显式地自己打开它。在脚本内部引用单个子进程的正确方法是通过PID,即使如此,等待进程完成也是自动的,除非它们被你的代码显式地后台化(使用&),或者它们自行分离(在这种情况下,wait甚至不起作用,并且由于双重fork过程用于自我后台化,给shell的PID将无效)。 - Charles Duffy
1
经过仔细研究,似乎闰秒被排除在UNIX时间戳之外,以至于某些时间戳涉及两秒的空间。这显然使得在亚秒范围内实现´gettimeofday`非常有趣,我想我们应该算是幸运的,因为闰秒从未从一年中删除。这意味着我必须更正自己:将86400秒添加到Unix时间戳中可以说总是与添加一天相同,因为没有办法特别引用2016-12-31T23:59:60。TIL。 - Wintermute
2
运行你的第一个代码(sh test.sh)给我报错:date: illegal option -- I usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ... [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format] - dorien
4
对于 macOS 来说,这个方法行不通,需要先安装 GNU date。请参考以下链接:https://apple.stackexchange.com/questions/231224/how-to-have-gnus-date-in-os-x - Jaime Agudo
显示剩余13条评论

33

花括号展开

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 -n -t"-" -k1 -k2 -k3) )
$ 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

5
闰年怎么处理? - Wintermute
1
@SirBenBenji 这取决于 executeJobs.py - kojiro
哦,哇。简单中蕴含着如此强大的力量。 - crafter
@NoahGary 我猜你想支持年份不仅仅是相同位数的情况。但如果我们处理的日期早于公元1000年,难道我们没有一堆其他问题要解决吗? - kojiro
@kojiro 绝对是一个边缘情况...以及闰年... 修复闰年很容易...但我认为这个答案展示了如何简单地生成带有括号扩展的列表...如果你理解它在做什么,它可以用于生成任何类型的日历。 - Noah Gary
显示剩余3条评论

21
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 $(date -d $start +%Y-%m-%d)
        start=$(date -d"$start + 1 day" +"%Y%m%d")

done

2
小心,这个程序利用了bash不检查变量类型的特点。它只适用于日期格式YMD——因为它将日期连接成类似整数的形式,从而使两个日期可比较。虽然20200201被认为小于20200202,但它不能处理2020-02-01和2020-02-02。OP要求使用破折号分隔的日期。 - n.r.
@n.r. 很好的警示建议。现在它也可以使用带破折号分隔的日期。 - petermeissner

6

@Gilli 提供的解决方案很聪明,因为它利用了这样一个事实:你可以简单地格式化两个日期,使它们看起来像整数。然后你可以使用-le/less-equal(通常只适用于数字数据)。

问题是,这会将您绑定到日期格式YMD,如20210201。如果您需要其他格式,比如2021-02-01(这是OP对要求的暗示),则该脚本将无法正常工作:

start='2021-02-01'
end='2021-02-05'

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

输出将会长成这样:
2021-02-01
2021-02-02
2021-02-03
2021-02-04
2021-02-05
2021-02-06
2021-02-07
./loop.sh: line 16: [[: 2021-02-08: value too great for base (error token is "08")

为了解决这个问题并使用此循环进行自定义日期格式,您需要使用一个额外的变量,让我们称其为“d_start”。
d_start='2021-02-01'
end='2021-02-05'

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

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

done

这将导致以下输出:
2021-02-01
2021-02-02
2021-02-03
2021-02-04
2021-02-05

5

我需要在AIX、BSDs、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 代码:

#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;
}

5

如果你被卡在像alpine这样的许多发行版中常用于docker容器中的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秒。

这是唯一一个在某些容器内部工作的。 - Eric
1
@user218867,这是因为大多数轻量级容器操作系统(如Alpine)只将日期打包到busybox中,而没有GNU date以节省空间。 - Gellweiler

4

Bash 最好通过利用管道(|)编写。这应该可以实现内存高效和并发(更快)处理。我会写以下内容:

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

以下代码将打印出日期为2020年8月20日,并打印出它之前100天的日期。
这个一行代码可以制作成一个实用程序。
#!/usr/bin/env bash

# date-range template <template>

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

export LANG;

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

默认情况下,我们选择每次迭代向过去1天。

$ 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

假设我们想生成某个日期之前的所有日期。我们还不知道需要多少次迭代才能到达那里。假设Tom出生于2001年1月1日。我们想生成每个日期,直到某个特定日期。我们可以使用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

我创建了一个date-seq存储库,对这个想法进行了改进并更好地记录了它。https://github.com/bas080/date-seq - bas080

4

我也遇到了同样的问题,我尝试了上面一些答案,可能它们是可以的,但是没有一个能解决我所尝试的使用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

希望能对您有所帮助。

我建议不要使用一个变量名,恰好与一个非常强大的逐位复制命令重合,但这只是我的个人看法。 - MerrillFraz
更简单的方法是在 macOS 中使用 gdate 而不是 date - 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

0
这可能也有所帮助。基于Gilli的答案,但是使用了一个不同的整数转换解决方案。
基本上,在验证输入时,LoopEachDay将“结束”日期以为单位存储,并首先将当前日期转换为秒(date -d“$dateIteration”'+%s')进行比较。
#/bin/bash

RegexVerify()
{
    regex="$1";
    shift;

    if [[ "$@" =~ $regex ]];
    then
        return 0;
    fi

    return 1;
}

VerifyDateISO8601()
{
    if RegexVerify '^[0-9]{4}-(0?[1-9]|10|11|12)-(0?[1-9]|[12][0-9]|3[01])$' "$1";
    then
        return 0;
    fi

    return 1;
}

# Iterate each day
#
# * The *first* argument is an ISO8601 start date.
# * The *second* argument is an ISO8601 end date or an empty string which assumes
# the current date.
LoopEachDay()
{
    if ! VerifyDateISO8601 "$1";
    then
        return 1;
    fi

    if ! VerifyDateISO8601 "$2" && [ "$2" != '' ];
    then
        return 2;
    fi

    dateIteration="$(date -d "$1" '+%Y-%m-%d')";
    dateIterationEndSeconds="$(date -d "$2" '+%s')";

    while (("$(date -d "$dateIteration" '+%s')" <= dateIterationEndSeconds))
    do
        printf $'%s\n' "$dateIteration"; # A work with "$dateIteration"

        dateIteration="$(date -d "$dateIteration + 1 day" '+%Y-%m-%d')";
    done
}

LoopEachDay '2021-13-01' '';
printf $'Exit code: %s\n\n' "$?";

# Exit code: 1

LoopEachDay '2021-04-01' '';

# 2021-04-01
# 2021-04-02
# 2021-04-03
# 2021-04-04
# 2021-04-05
# 2021-04-06
# 2021-04-07
# 2021-04-08

printf $'\n';
LoopEachDay '2021-04-03' '2021-04-06';

# 2021-04-03
# 2021-04-04
# 2021-04-05
# 2021-04-06

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接