【问题标题】:How to loop through dates using Bash?如何使用 Bash 遍历日期?
【发布时间】:2015-03-29 08:36:16
【问题描述】:

我有这样的 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 年 1 月 1 日至 2015 年 1 月 31 日。

如何在 Bash 中实现?

更新

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

例如我可以在循环中加入wait%1 吗?

【问题讨论】:

  • 您在使用 GNU date 的平台上吗?
  • 顺便说一句,由于您手边有一个 Python 解释器,因此使用 datetime Python 模块以可靠和可移植的方式更容易做到这一点。
  • 2015-01-01 到 2015-01-31 不跨越超过一个月的日期,所以这是一个非常简单的案例。
  • ...所以,如果您确实看到wait需要(例如,当您不这样做时,由于并发进程而发生错误),那么你有一些更有趣/更复杂的事情,需要一个更复杂的解决方案(比如要求子进程继承一个锁文件),这已经足够复杂并且与日期算术足够无关,应该是一个单独的问题。

标签: bash loops date


【解决方案1】:

使用 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

为了万年以后的安全,当字典比较失效时,使用

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,其思想是这种形式的日期可以进行数字比较。

【讨论】:

  • 当然,为什么不呢。它只是一个 shell 循环,它使用日期作为迭代器并不会改变您在其中可以执行的操作。
  • @SirBenBenji, ...也就是说,%1 是一个作业控制结构,并且在非交互式脚本中作业控制是关闭的,除非您自己明确打开它。引用脚本中各个子进程的正确方法是通过 PID,即使这样,等待进程完成也是自动,除非它们被您的代码明确作为背景(如&amp;) ,或者它们自分离(在这种情况下,wait 甚至无法工作,并且给 shell 的 PID 将被用于自背景的双叉进程无效)。
  • 仔细查看后,似乎闰秒已从 UNIX 时间戳中排除,因此某些时间戳指的是两秒的空间。这显然使得在亚秒范围内实现“gettimeofday”非常有趣,我想我们应该庆幸闰秒从未从一年中删除。这意味着我必须纠正自己:将 86400 秒添加到 unix 时间戳可以说总是与添加一天相同,因为没有办法专门引用 2016-12-31T23:59:60。直到。
  • 运行你的第一个代码 (sh test.sh) 给我一个错误:日期:非法选项 -- 我的用法:日期 [-jnRu] [-d dst] [-r seconds] [-t西] [-v[+|-]val[ymwdHMS]] ... [-f fmt 日期 | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+格式]
  • macOS不行,先安装gnu dateapple.stackexchange.com/questions/231224/…
【解决方案2】:

Brace expansion:

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

【讨论】:

  • 闰年呢?
  • 我可以使用python /home/user/executeJobs.py 2015-01-{01..31} &amp;&gt; /home/user/2015-01-{01..31}.log 吗?
  • @SirBenBenji 这取决于executeJobs.py
  • 我必须说 executeJobs 需要 date 参数,我需要等待每次运行完成。这是一项大数据作业,在任何情况下都不应在之前的每次运行完成之前开始。我早该想到这一点,抱歉忘记了。
【解决方案3】:
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

【讨论】:

  • 小心,这个玩笑了 bash 不检查变量类型的事实。它仅适用于日期格式 YMD - 因为它将日期连接成看起来像整数的东西,这使得两个日期具有可比性。虽然 20200201 将被视为小于 20200202,但它不适用于 2020-02-01 和 2020-02-02。 OP 要求使用破折号分隔的日期。
  • @n.r.良好的警示建议。现在它也适用于虚线分隔的日期。
【解决方案4】:

我需要遍历 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代码:

#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】:

    @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
    

    【讨论】:

      【解决方案6】:

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

      希望对你有帮助。

      【讨论】:

      • 我建议不要使用恰好与非常强大的逐位复制命令相吻合的变量名称,但这只是我的。
      • 更简单的方法是在macOS中使用gdate而不是date
      【解决方案7】:

      最好使用管道(|)来编写 Bash。这应该会导致内存高效和并发(更快)处理。我会写以下内容:

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

      下面会打印20 aug 2020的日期,并打印它之前100天的日期。

      这个oneliner可以变成一个实用程序。

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

      假设我们要生成直到某个日期的日期。我们还不知道我们需要多少次迭代才能到达那里。假设汤姆出生于 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
      

      【讨论】:

      【解决方案8】:

      如果您对 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 秒。

      【讨论】:

      • 这是唯一在某个容器内工作的...
      • @user218867 是的,这是因为像 Alpine 这样的大多数轻量级容器操作系统只有将日期打包到busybox中,而没有 GNU 日期以节省空间。
      【解决方案9】:

      如果想从输入日期循环到以下任何范围,它也会以 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
      

      【讨论】:

        【解决方案10】:

        这也可能有所帮助。基于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
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2022-01-24
          • 1970-01-01
          • 2017-12-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-12-23
          相关资源
          最近更新 更多