Unix Shell脚本中的日期算术
我需要在用于控制第三方程序执行的Unix Shell脚本中执行日期算术。
我正在使用一个函数来增加一天,另一个来减少:
IncrementaDia(){ echo | awk ' BEGIN { diasDelMes[1] = 31 diasDelMes[2] = 28 diasDelMes[3] = 31 diasDelMes[4] = 30 diasDelMes[5] = 31 diasDelMes[6] = 30 diasDelMes[7] = 31 diasDelMes[8] = 31 diasDelMes[9] = 30 diasDelMes[10] = 31 diasDelMes[11] = 30 diasDelMes[12] = 31 } { anio=substr(,1,4) mes=substr(,5,2) dia=substr(,7,2) if((anio % 4 == 0 && anio % 100 != 0) || anio % 400 == 0) { diasDelMes[2] = 29; } if( dia == diasDelMes[int(mes)] ) { if( int(mes) == 12 ) { anio = anio + 1 mes = 1 dia = 1 } else { mes = mes + 1 dia = 1 } } else { dia = dia + 1 } } END { printf("%04d%02d%02d", anio, mes, dia) } ' } if [ $# -eq 1 ]; then tomorrow= else today=$(date +"%Y%m%d") tomorrow=$(IncrementaDia $hoy) fi
但是现在我需要做更复杂的算术运算。
什么是最好的且更兼容的方法?
解决方案
回答
假设我们有GNU日期,如下所示:
date --date='1 days ago' '+%a'
和类似的短语。
回答
date --date='1 days ago' '+%a'
这不是一个非常兼容的解决方案。它仅在Linux中有效。至少,它在Aix和Solaris中不起作用。
它在RHEL中有效:
date --date='1 days ago' '+%Y%m%d' 20080807
回答
为什么不使用perl或者python这样的语言来编写脚本,而该语言更自然地支持复杂的日期处理?当然,我们可以在bash中完成所有操作,但是我认为,例如,只要可以确保安装了perl或者python,就可以在跨平台使用python的情况下获得更高的一致性。
我应该补充一点,很容易将python和perl脚本连接到包含外壳程序的脚本中。
回答
进一步研究,我认为我们可以只使用日期。
我已经在OpenBSD上尝试了以下方法:我采用了2008年2月29日和一个随机小时(以080229301535的形式)的日期,并为日部分添加了+1,如下所示:
$ date -j 0802301535 Sat Mar 1 15:35:00 EST 2008
如我们所见,日期正确地格式化了时间...
高温超导
回答
如果date的GNU版本适合我们,为什么不获取源代码并在AIX和Solaris上对其进行编译?
http://www.gnu.org/software/coreutils/
无论如何,如果我们要编写自己的代码,则源代码应该可以正确设置日期算术。
顺便说一句,诸如"该解决方案很好,但我们肯定会注意到它不尽人意。这样的注释似乎似乎没有人想到在构造Unix时修改日期"。真的不能把我们带到任何地方。我发现到目前为止的每一项建议都是非常有用的,而且有针对性。
回答
要在UNIX上对日期进行算术运算,我们需要获得以UNIX时代以来的秒数为单位的日期,进行一些计算,然后转换回可打印的日期格式。 date命令应该既可以给我们提供自纪元以来的秒数,又可以从该数字转换回可打印的日期。我的本地日期命令可以做到这一点,
% date -n 1219371462 % date 1219371462 Thu Aug 21 22:17:42 EDT 2008 %
请参见本地的" date(1)"手册页。
要增加一天,请增加86400秒。
回答
我碰到过几次了。我的想法是:
- 日期算术总是很痛苦
- 使用EPOCH日期格式会容易一些
- Linux上的日期转换为EPOCH,但在Solaris上不转换
- 在日期部分使用perl(大多数unix安装都包括perl,因此我通常会认为此操作不需要其他工作)。
一个示例脚本(检查某些用户文件的使用期限,以查看是否可以删除该帐户):
#!/usr/local/bin/perl $today = time(); $user = $ARGV[0]; $command="awk -F: '/$user/ {print $6}' /etc/passwd"; chomp ($user_dir = `$command`); if ( -f "$user_dir/.sh_history" ) { @file_dates = stat("$user_dir/.sh_history"); $sh_file_date = $file_dates[8]; } else { $sh_file_date = 0; } if ( -f "$user_dir/.bash_history" ) { @file_dates = stat("$user_dir/.bash_history"); $bash_file_date = $file_dates[8]; } else { $bash_file_date = 0; } if ( $sh_file_date > $bash_file_date ) { $file_date = $sh_file_date; } else { $file_date = $bash_file_date; } $difference = $today - $file_date; if ( $difference >= 3888000 ) { print "User needs to be disabled, 45 days old or older!\n"; exit (1); } else { print "OK\n"; exit (0); }
回答
如果要继续使用awk,则mktime和strftime函数非常有用:
BEGIN { dateinit } { newdate=daysadd(OldDate,DaysToAdd)} # daynum: convert DD-MON-YYYY to day count #----------------------------------------- function daynum(date, d,m,y,i,n) { y=substr(date,8,4) m=gmonths[toupper(substr(date,4,3))] d=substr(date,1,2) return mktime(y" "m" "d" 12 00 00") } #numday: convert day count to DD-MON-YYYY #------------------------------------------- function numday(n, y,m,d) { m=toupper(substr(strftime("%B",n),1,3)) return strftime("%d-"m"-%Y",n) } # daysadd: add (or subtract) days from date (DD-MON-YYYY), return new date (DD-MON-YYYY) #------------------------------------------ function daysadd(date, days) { return numday(daynum(date)+(days*86400)) } #init variables for date calcs #----------------------------------------- function dateinit( x,y,z) { # Stuff for date calcs split("JAN:1,FEB:2,MAR:3,APR:4,MAY:5,JUN:6,JUL:7,AUG:8,SEP:9,OCT:10,NOV:11,DEC:12", z) for (x in z) { split(z[x],y,":") gmonths[y[1]]=y[2] } }
回答
克里斯·约翰逊(Chris F.A. Johnson)撰写的《 Shell脚本食谱:问题解决方法》(ISBN:978-1-59059-471-1)一书中的日期函数库可能会有所帮助。源代码位于http://apress.com/book/downloadfile/2146(日期函数位于tar文件中的Chapter08 / data-funcs-sh中)。
回答
我编写了一个bash脚本,用于将英语表示的日期转换为常规日期
mm / dd / yyyy日期。它称为ComputeDate。
这是使用它的一些例子。为简便起见,我放置了每次调用的输出
与调用位于同一行,并用冒号(:)分隔。运行ComputeDate时,以下所示的引号不是必需的:
$ ComputeDate 'yesterday': 03/19/2010 $ ComputeDate 'yes': 03/19/2010 $ ComputeDate 'today': 03/20/2010 $ ComputeDate 'tod': 03/20/2010 $ ComputeDate 'now': 03/20/2010 $ ComputeDate 'tomorrow': 03/21/2010 $ ComputeDate 'tom': 03/21/2010 $ ComputeDate '10/29/32': 10/29/2032 $ ComputeDate 'October 29': 10/1/2029 $ ComputeDate 'October 29, 2010': 10/29/2010 $ ComputeDate 'this monday': 'this monday' has passed. Did you mean 'next monday?' $ ComputeDate 'a week after today': 03/27/2010 $ ComputeDate 'this satu': 03/20/2010 $ ComputeDate 'next monday': 03/22/2010 $ ComputeDate 'next thur': 03/25/2010 $ ComputeDate 'mon in 2 weeks': 03/28/2010 $ ComputeDate 'the last day of the month': 03/31/2010 $ ComputeDate 'the last day of feb': 2/28/2010 $ ComputeDate 'the last day of feb 2000': 2/29/2000 $ ComputeDate '1 week from yesterday': 03/26/2010 $ ComputeDate '1 week from today': 03/27/2010 $ ComputeDate '1 week from tomorrow': 03/28/2010 $ ComputeDate '2 weeks from yesterday': 4/2/2010 $ ComputeDate '2 weeks from today': 4/3/2010 $ ComputeDate '2 weeks from tomorrow': 4/4/2010 $ ComputeDate '1 week after the last day of march': 4/7/2010 $ ComputeDate '1 week after next Thursday': 4/1/2010 $ ComputeDate '2 weeks after the last day of march': 4/14/2010 $ ComputeDate '2 weeks after 1 day after the last day of march': 4/15/2010 $ ComputeDate '1 day after the last day of march': 4/1/2010 $ ComputeDate '1 day after 1 day after 1 day after 1 day after today': 03/24/2010
我已将此脚本作为对此问题的答案,因为它说明了如何
通过一组bash函数进行日期算术,这些函数可能很有用
为他人。它可以正确处理leap年和and百年:
#! /bin/bash # ConvertDate -- convert a human-readable date to a MM/DD/YY date # # Date ::= Month/Day/Year # | Month/Day # | DayOfWeek # | [this|next] DayOfWeek # | DayofWeek [of|in] [Number|next] weeks[s] # | Number [day|week][s] from Date # | the last day of the month # | the last day of Month # # Month ::= January | February | March | April | May | ... | December # January ::= jan | january | 1 # February ::= feb | january | 2 # ... # December ::= dec | december | 12 # Day ::= 1 | 2 | ... | 31 # DayOfWeek ::= today | Sunday | Monday | Tuesday | ... | Saturday # Sunday ::= sun* # ... # Saturday ::= sat* # # Number ::= Day | a # # Author: Larry Morell if [ $# = 0 ]; then printdirections$ echo a b c d e | col 5 3 2exit fi # Request the value of a variable GetVar () { Var= echo -n "$Var= [${!Var}]: " local X read X if [ ! -z $X ]; then eval $Var="$X" fi } IsLeapYear () { local Year= if [ $[20$Year % 4] -eq 0 ]; then echo yes else echo no fi } # AddToDate -- compute another date within the same year DayNames=(mon tue wed thu fri sat sun ) # To correspond with 'date' output Day2Int () { ErrorFlag= case in -e ) ErrorFlag=-e; shift ;; esac local dow= n=0 while [ $n -lt 7 -a $dow != "${DayNames[n]}" ]; do let n++ done if [ -z "$ErrorFlag" -a $n -eq 7 ]; then echo Cannot convert $dow to a numeric day of wee exit fi echo $[n+1] } Months=(31 28 31 30 31 30 31 31 30 31 30 31) MonthNames=(jan feb mar apr may jun jul aug sep oct nov dec) # Returns the month (1-12) from a date, or a month name Month2Int () { ErrorFlag= case in -e ) ErrorFlag=-e; shift ;; esac M= Month=${M%%/*} # Remove /... case $Month in [a-z]* ) Month=${Month:0:3} M=0 while [ $M -lt 12 -a ${MonthNames[M]} != $Month ]; do let M++ done let M++ esac if [ -z "$ErrorFlag" -a $M -gt 12 ]; then echo "'$Month' Is not a valid month." exit fi echo $M } # Retrieve month,day,year from a legal date GetMonth() { echo ${1%%/*} } GetDay() { echo | col / 2 } GetYear() { echo ${1##*/} } AddToDate() { local Date= local days= local Month=`GetMonth $Date` local Day=`echo $Date | col / 2` # Day of Date local Year=`echo $Date | col / 3` # Year of Date local LeapYear=`IsLeapYear $Year` if [ $LeapYear = "yes" ]; then let Months[1]++ fi Day=$[Day+days] while [ $Day -gt ${Months[$Month-1]} ]; do Day=$[Day - ${Months[$Month-1]}] let Month++ done echo "$Month/$Day/$Year" } # Convert a date to normal form NormalizeDate () { Date=`echo "$*" | sed 'sX *X/Xg'` local Day=`date +%d` local Month=`date +%m` local Year=`date +%Y` #echo Normalizing Date=$Date > /dev/tty case $Date in */*/* ) Month=`echo $Date | col / 1 ` Month=`Month2Int $Month` Day=`echo $Date | col / 2` Year=`echo $Date | col / 3` ;; */* ) Month=`echo $Date | col / 1 ` Month=`Month2Int $Month` Day=1 Year=`echo $Date | col / 2 ` ;; [a-z]* ) # Better be a month or day of week Exp=${Date:0:3} case $Exp in jan|feb|mar|apr|may|june|jul|aug|sep|oct|nov|dec ) Month=$Exp Month=`Month2Int $Month` Day=1 #Year stays the same ;; mon|tue|wed|thu|fri|sat|sun ) # Compute the next such day local DayOfWeek=`date +%u` D=`Day2Int $Exp` if [ $DayOfWeek -le $D ]; then Date=`AddToDate $Month/$Day/$Year $[D-DayOfWeek]` else Date=`AddToDate $Month/$Day/$Year $[7+D-DayOfWeek]` fi # Reset Month/Day/Year Month=`echo $Date | col / 1 ` Day=`echo $Date | col / 2` Year=`echo $Date | col / 3` ;; * ) echo "$Exp is not a valid month or day" exit ;; esac ;; * ) echo "$Date is not a valid date" exit ;; esac case $Day in [0-9]* );; # Day must be numeric * ) echo "$Date is not a valid date" exit ;; esac [0-9][0-9][0-9][0-9] );; # Year must be 4 digits [0-9][0-9] ) Year=20$Year ;; esac Date=$Month/$Day/$Year echo $Date } # NormalizeDate jan # NormalizeDate january # NormalizeDate jan 2009 # NormalizeDate jan 22 1983 # NormalizeDate 1/22 # NormalizeDate 1 22 # NormalizeDate sat # NormalizeDate sun # NormalizeDate mon ComputeExtension () { local Date=; shift local Month=`GetMonth $Date` local Day=`echo $Date | col / 2` local Year=`echo $Date | col / 3` local ExtensionExp="$*" case $ExtensionExp in *w*d* ) # like 5 weeks 3 days or even 5w2d ExtensionExp=`echo $ExtensionExp | sed 's/[a-z]/ /g'` weeks=`echo $ExtensionExp | col 1` days=`echo $ExtensionExp | col 2` days=$[7*weeks+days] Due=`AddToDate $Month/$Day/$Year $days` ;; *d ) # Like 5 days or 5d ExtensionExp=`echo $ExtensionExp | sed 's/[a-z]/ /g'` days=$ExtensionExp Due=`AddToDate $Month/$Day/$Year $days` ;; * ) Due=$ExtensionExp ;; esac echo $Due } # Pop -- remove the first element from an array and shift left Pop () { Var= eval "unset $Var[0]" eval "$Var=(${$Var[*]})" } ComputeDate () { local Date=`NormalizeDate `; shift local Expression=`echo $* | sed 's/^ *a /1 /;s/,/ /' | tr A-Z a-z ` local Exp=(`echo $Expression `) local Token=$Exp # first one local Ans= #echo "Computing date for ${Exp[*]}" > /dev/tty case $Token in */* ) # Regular date M=`GetMonth $Token` D=`GetDay $Token` Y=`GetYear $Token` if [ -z "$Y" ]; then Y=$Year elif [ ${#Y} -eq 2 ]; then Y=20$Y fi Ans="$M/$D/$Y" ;; yes* ) Ans=`AddToDate $Date -1` ;; tod*|now ) Ans=$Date ;; tom* ) Ans=`AddToDate $Date 1` ;; the ) case $Expression in *day*after* ) #the day after Date Pop Exp; # Skip the Pop Exp; # Skip day Pop Exp; # Skip after #echo Calling ComputeDate $Date ${Exp[*]} > /dev/tty Date=`ComputeDate $Date ${Exp[*]}` #Recursive call #echo "New date is " $Date > /dev/tty Ans=`AddToDate $Date 1` ;; *last*day*of*th*month|*end*of*th*month ) M=`date +%m` Day=${Months[M-1]} if [ $M -eq 2 -a `IsLeapYear $Year` = yes ]; then let Day++ fi Ans=$Month/$Day/$Year ;; *last*day*of* ) D=${Expression##*of } D=`NormalizeDate $D` M=`GetMonth $D` Y=`GetYear $D` # echo M is $M > /dev/tty Day=${Months[M-1]} if [ $M -eq 2 -a `IsLeapYear $Y` = yes ]; then let Day++ fi Ans=$[M]/$Day/$Y ;; * ) echo "Unknown expression: " $Expression exit ;; esac ;; next* ) # next DayOfWeek Pop Exp dow=`Day2Int $DayOfWeek` # First 3 chars tdow=`Day2Int ${Exp:0:3}` # First 3 chars n=$[7-dow+tdow] Ans=`AddToDate $Date $n` ;; this* ) Pop Exp dow=`Day2Int $DayOfWeek` tdow=`Day2Int ${Exp:0:3}` # First 3 chars if [ $dow -gt $tdow ]; then echo "'this $Exp' has passed. Did you mean 'next $Exp?'" exit fi n=$[tdow-dow] Ans=`AddToDate $Date $n` ;; [a-z]* ) # DayOfWeek ... M=${Exp:0:3} case $M in jan|feb|mar|apr|may|june|jul|aug|sep|oct|nov|dec ) ND=`NormalizeDate ${Exp[*]}` Ans=$ND ;; mon|tue|wed|thu|fri|sat|sun ) dow=`Day2Int $DayOfWeek` Ans=`NormalizeDate $Exp` if [ ${#Exp[*]} -gt 1 ]; then # Just a DayOfWeek #tdow=`GetDay $Exp` # First 3 chars #if [ $dow -gt $tdow ]; then #echo "'this $Exp' has passed. Did you mean 'next $Exp'?" #exit #fi #n=$[tdow-dow] #else # DayOfWeek in a future week Pop Exp # toss monday Pop Exp # toss in/off if [ $Exp = next ]; then Exp=2 fi n=$[7*(Exp-1)] # number of weeks n=$[n+7-dow+tdow] Ans=`AddToDate $Date $n` fi ;; esac ;; [0-9]* ) # Number weeks [from|after] Date n=$Exp Pop Exp; case $Exp in w* ) let n=7*n;; esac Pop Exp; Pop Exp #echo Calling ComputeDate $Date ${Exp[*]} > /dev/tty Date=`ComputeDate $Date ${Exp[*]}` #Recursive call #echo "New date is " $Date > /dev/tty Ans=`AddToDate $Date $n` ;; esac echo $Ans } Year=`date +%Y` Month=`date +%m` Day=`date +%d` DayOfWeek=`date +%a |tr A-Z a-z` Date="$Month/$Day/$Year" ComputeDate $Date $*
该脚本大量使用了我写的另一个脚本(对使用Linux随附的标准col的人表示歉意。)这个版本的
col简化了从标准输入中提取列的操作。因此,
e c b
印刷
#!/bin/sh # col -- extract columns from a file # Usage: # col [-r] [c] col-1 col-2 ... # where [c] if supplied defines the field separator # where each col-i represents a column interpreted according to the presence of -r as follows: # -r present : counting starts from the right end of the line # -r absent : counting starts from the left side of the line Separator=" " Reverse=false case "" in -r ) Reverse=true; shift; ;; [0-9]* ) ;; * )Separator=""; shift; ;; esac case "" in -r ) Reverse=true; shift; ;; [0-9]* ) ;; * )Separator=""; shift; ;; esac # Replace each col-i with $i Cols="" for f in $* do if [ $Reverse = true ]; then Cols="$Cols $(NF-$f+1)," else Cols="$Cols $$f," fi done Cols=`echo "$Cols" | sed 's/,$//'` #echo "Using column specifications of $Cols" awk -F "$Separator" "{print $Cols}"
这里是col脚本:
#!/bin/sh # # printdirections -- print header lines of a shell script # # Usage: # printdirections path # where # path is a *full* path to the shell script in question # beginning with '/' # # To use printdirections, you must include (as comments at the top # of your shell script) documentation for running the shell script. if [ $# -eq 0 -o "$*" = "-h" ]; then printdirections$ chmod a+x ComputeDate col printdirectionsexit fi # Delete the command invocation at the top of the file, if any # Delete from the place where printdirections occurs to the end of the file # Remove the # comments # There is a bizarre oddity here. sed '/#!/d;/.*printdirections/,$d;/ *#/!d;s/# //;s/#//' > /tmp/printdirections.$$ # Count the number of lines numlines=`wc -l /tmp/printdirections.$$ | awk '{print }'` # Remove the last line numlines=`expr $numlines - 1` head -n $numlines /tmp/printdirections.$$ rm /tmp/printdirections.$$
当脚本被不正确地调用时,它还使用printdirections打印出指令:
##代码##要使用此脚本,请将三个脚本分别放在文件ComputeDate,col和printdirections中。将文件放在PATH命名的目录中,通常为〜/ bin。然后使用以下命令使它们可执行:
##代码##问题?给我发送电子邮件:morell at cs.atu.edu将ComputeDate放在主题中。