很多人第一次在Linux终端里敲cal,多半是顺手看一眼这个月几号是周末、今天标在哪里,然后就没有然后了。我这些年带人、写脚本、做运维排期,越来越觉得cal是所有常用命令里最被低估的一个——命令大全里它往往只占一行,但真正用熟的人,能把这一行拆出不少花活。这篇文章不打算讲什么高深理论,就是把我实际用cal时反复验证过的姿势、踩过的坑和可以直接抄走的命令组合整理出来。无论你是刚碰Linux的新手,还是已经写了几年脚本的工程师,应该都能从中找到点东西。
1. 先把基本玩法摸透:几条命令就够日常用
1.1 五条调用姿势,记住了就不慌
cal的基本形式是cal [参数] [[月份] 年份],大多数时候我们只需要五条:
| 命令 | 作用 |
|---|---|
cal |
显示当前月 |
cal 2025 |
显示2025年全年,共12个月 |
cal 8 2025 |
显示2025年8月 |
cal -3 |
显示上个月、当前月、下个月 |
cal -y |
显示今年的全年日历 |
这里最容易被新手问倒的是“先月后年”这个顺序。cal 8 2025是2025年8月,cal 2025 8会直接报错。原因很简单:命令解析器把第一个数字当月份,第二个当年份,顺序反了自然对不上。我习惯把这条写成“小数字在前,大数字在后”,和日常说“8月2025年”的顺序一致,不容易错。
1.2 输出里藏着的信息,别只盯着日期数字
如果你在中文locale下执行cal,输出大概是这样的:
text复制 八月 2025
日 一 二 三 四 五 六
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
第一行是“月份 年份”,第二行是星期表头,后面才是日期。有几个细节值得注意:
- 今天这个数字在终端里会被高亮,一般表现为下划线或反白。这个高亮只在标准输出是tty时出现,一旦你用管道把结果传给
grep或重定向到文件,高亮就会消失。所以脚本里不要依赖这个高亮来判断日期,用date +%d更靠谱。 - 每个日期占两列,两个日期之间用一个空格隔开。也就是说,每一格的基本宽度是3个字符。这个规律是后面用awk解析日历的根基。
- 日历每周结束后会有一个空行,方便人眼区分周与周,但对写脚本的人来说这是最常见的干扰源,后面会讲怎么过滤。
如果你用LC_ALL=C cal,星期表头会变成英文缩写Su Mo Tu We Th Fr Sa。很多人觉得英文版不如中文亲切,但在脚本里英文表头反而更稳定,后面有一节专门说这个问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四个关键参数,让日历替你排期
2.1 前后三个月:-3 比翻页快得多
cal -3会并排显示上个月、本月和下个月,一眼就能看清跨月的日期落在星期几。我排发布窗口、安排值班表的时候经常用它,因为很多计划是“本月最后一个周五到下月第二个周一”这种跨月区间,只看单月日历很容易看串。
如果觉得三个月不够,可以用-A和-B指定往后、往前几个月。比如cal -A 2 -B 1表示“往前1个月、往后2个月”,共4个月。在util-linux版本里这个参数是通用的,具体数值可以按需调整。需要注意的是,有的旧版本或BSD版本不支持-A/-B,如果你在macOS上跑,先man cal确认一下。
2.2 全年十二张图:-y 与 cal 年份
cal 2025会直接输出2025年全部12个月,默认按每行3个月排成4行,正好是挂在墙上那种年历的样子。cal -y则是“今年的全年”,适合每年年初打印一份做年度规划。
如果你只是想知道某个月,不要用cal -y再翻半天,直接cal 4 2025。如果全年输出太长,还可以配合less分页看:cal 2025 | less。这个组合特别适合在终端里快速翻阅,比打开图形日历软件还快。
2.3 周一开头和周号:-m、-s、-w
默认情况下,cal把周日作为每周第一天。中文习惯里更常用“周一是一周之始”,这时可以用cal -m。比如cal -m 8 2025,星期日一栏就移到最右。
有些和我一样做排期的同事喜欢看“第几周”,较新版本的util-linux支持-w或--week,它会在每行左侧标出ISO周号,并且隐含以周一作为一周第一天。这个对“本月第几周”这类计算特别友好,但不同发行版支持程度不一样。我在CentOS 7上见过不支持-w的老版本,所以用到之前先执行man cal查一下你的环境里有没有这个选项,别在脚本里写死。
2.4 换个视角看日历:ncal 与 -j
一些系统上还提供ncal命令,它和cal一样是看日历,但表现形式不一样。ncal把同一个星期几拉到同一行,比如想看“8月都有哪几个周五”,直接扫一眼ncal 8 2025里的对应行即可,非常直观。Debian系里ncal在bsdmainutils包里,不是所有发行版都默认带,没有的话可以用包管理器装。
cal -j则是把每个日期替换成“这一年的第几天”。例如6月15日会显示为第166天。它的定位更偏计算:想知道某天离年末还有多少天、或者快速算两个日期之间的日差时,比掰着手指头方便。不过真要写脚本,我更常用date -d '2025-06-15' +%j,因为date命令直接输出数值,不用再去解析日历文本。
3. 脚本里这样用cal,才叫真会
3.1 一行命令取“某月有几天”
这是我从老运维那儿学来的经典写法:
bash复制cal 8 2025 | awk 'NF {days = $NF} END {print days}'
原理非常简单:cal输出的所有日期行里,最后一个非空字段就是当月最后一天的数字。awk里NF表示非空行,$NF是最后一个字段,于是这个命令遍历所有非空行,不断用最后一个字段覆盖变量,最后输出的就是月末号。对8月是31,对2月可能是28或29。
把它扩到全年,就是一张全年天数速查表:
bash复制for m in $(seq 1 12); do
days=$(cal "$m" 2025 | awk 'NF {d=$NF} END {print d}')
echo "$m 月:$days 天"
done
这个技巧在写排班、计费、报表脚本时特别常见,因为它不依赖任何第三方工具,属于“裸系统即可运行”的写法。
3.2 判断星期几:最稳的其实不是cal
Linux下判断某天是星期几,第一选择是date:
bash复制date -d 2025-08-15 +%u
%u输出1到7,1是周一,7是周日;%A则直接输出“Friday”这种英文名。在macOS上,没有GNU date的-d参数,要用date -j -f "%Y-%m-%d" 2025-08-15 +%u。相比之下,用cal反推某天是星期几,还得解析列位置,脚本写起来绕一大圈,除非你所在的系统没有GNU date,否则我不建议为了这个去解析日历文本。
那cal在脚本里到底管什么?我的经验是:date负责“算”,cal负责“给人看”。你要的是一个一眼能懂的排期视图,用cal;你要的是一个能参与if判断的数值,用date。两者配合,效率远大于互相替代。
3.3 给日历打标:sed 替换今天和重点日
另一个我经常做的操作,是把日历里某个日期改写成自己的标注。比如想看2025年8月的日历,同时把15号标记成发版日:
bash复制cal -m 8 2025 | sed "s/ 15 / [发版] /"
如果想把“今天”自动标出来,可以组合date:
bash复制cal -m 8 2025 | sed "s/ $(date +%-d) / [今天] /"
date +%-d不会补前导零,正好匹配日历里两位宽度的写法。执行后,日历里那个数字会被替换成[今天],字符宽度变了,后面的列会错开一点。这其实是正常现象——你要的是标注醒目,不是继续拿它做文本解析。如果你需要“既标出来又不破坏对齐”,那就别用sed替换,而是在下方加一行备注,或者干脆不折腾。
4. 那些坑与彩蛋:版本差异和消失的十天
4.1 cal 25 不是 2025 年
这个坑我踩过一次之后印象极深:想看2025年的日历,条件反射地敲了cal 25,结果屏幕上出现了公元25年的全年日历。cal对单个数字参数的解析把它当成了“年份”,而且不要求一定是四位数。所以想看哪一年,就老老实实写全四个数字,cal 2025。两个数字的年份在部分版本里还可能被当作“年份全称”,比如cal 25是公元25年,而不是2025年,这种细节在脚本里尤其容易埋雷。
4.2 中文星期表头,脚本为什么解析失败
默认中文locale下,cal输出的星期表头是“日 一 二 三 四 五 六”。这个显示对人类很友好,但对脚本不友好:单字星期名在做列匹配时容易把“一”和“十一”里的“一”搞混,而且awk按空格拆字段时,中文和英文的宽度处理也不一样。
我的做法是在脚本里显式固定英文输出:
bash复制LC_ALL=C cal 8 2025
这样表头就变成Su Mo Tu We Th Fr Sa,日期列的位置是确定的。别小看这个细节,我在客户环境里排查过一个报表脚本,问题就出在部署机locale是中文,而脚本里写死在$1 == "Mo"去判断表头,结果拿到的是中文“一”,整个统计全部错位。加个LC_ALL=C,一分钟解决。
4.3 1582年10月,日历上少了十天
很多人第一次发现cal 10 1582显示“不正常”时,第一反应是命令坏了。其实这是cal在真实还原历法史:1582年10月,欧洲部分地区从儒略历切换到格里高利历时,为了修正春分误差,直接跳过了10月5日到14日。所以你会看到4日之后紧跟着15日。
类似的还有cal 9 1752,英国及其当时属地切换历法时,9月3日到13日消失。cal命令在实现时就考虑了这些历史断点,不是bug。顺带还能用它验证一个容易记错的闰年规则:世纪年必须能被400整除才是闰年,所以1900年不是闰年,2000年是。想看的话分别执行cal 2 1900 | tail -1和cal 2 2000 | tail -1,最后一行一个停在28,一个停在29。
4.4 常见问题速查表
| 现象 | 原因 | 解决 |
|---|---|---|
cal 25显示公元25年 |
单个数字被当作完整年份 | 写全四位数,如cal 2025 |
| 中文星期表头导致脚本错位 | locale影响输出语言 | 脚本里加LC_ALL=C cal |
| 管道或重定向后今天不高亮 | 高亮只在tty时输出 | 用date +%d判断今天 |
-A/-B参数报错 |
旧版本或BSD版本不支持 | 先man cal确认 |
cal 10 1582少10天 |
历法改革的历史断层 | 正常现象,不是命令故障 |
5. 实战小脚本:一张会提醒你的日历
5.1 先定义清楚:脚本要解决什么问题
前面拆了那么多,不如写一个能放生产环境用的小脚本。需求很简单:每月打印一份当月日历,把今天标出来,再自动列出几个固定节点,比如“每月1号做数据快照”“本月最后一个周五做走查”。一个mycal脚本就能搞定。
为什么选这个案例?因为它同时用得上三样东西:cal负责可视化、date负责日期计算、简单的循环负责自动找“最后一个周五”。学会这套组合,往后处理任何周期任务排期都能举一反三。
5.2 我的实现和完成后的样子
代码如下:
bash复制#!/usr/bin/env bash
# mycal:打印月历并标注关键节点
month="${1:-$(date +%-m)}"
year="${2:-$(date +%Y)}"
today="$(date +%-d)"
echo "===== $year 年 $month 月 ====="
cal -m "$month" "$year" | sed "s/ $today / [今天] /"
echo ""
echo "本月节点:"
echo " 01日 月度数据快照"
# 找本月的最后一个周五
last_friday=""
for d in $(seq 1 31); do
if date -d "$year-$month-$d" >/dev/null 2>&1; then
if [ "$(date -d "$year-$month-$d" +%u)" = "5" ]; then
last_friday="$d"
fi
fi
done
if [ -n "$last_friday" ]; then
echo " ${last_friday}日 月度走查与复盘"
fi
执行./mycal 8 2025,输出效果类似:
text复制===== 2025 年 8 月 =====
八月 2025
一 二 三 四 五 六 日
1 2
3 4 5 6 7 8 9
...
28 29 30 31 [今天]
本月节点:
01日 月度数据快照
29日 月度走查与复盘
循环里用date -d "$year-$month-$d"判断日期是否合法,自动避开了天数不足的月份;用%u判断星期,5表示周五,最后一轮循环覆盖到的就是最后一个周五。这个实现不算取巧,但胜在逻辑直白、可维护。
5.3 还能往哪儿扩展
我自己在实际使用中会把它再往前推几步:
- 结合
cron,每月1号自动生成一份带日期的日历文件,放到团队共享目录里,大家一进办公室就看得到。 - 在
cal输出上继续用sed替换更多日期,把值班、发版、备份窗口全标上去,形成一张“运维版月历”。 - 在桌面环境里把脚本输出接到
notify-send,每天登录时弹一次通知,提示今天有哪些节点。
有人可能会问,这些事用图形日历或者协作软件不都能做吗?确实能,但在纯终端环境、无桌面的服务器上,cal和date的组合是零依赖、立刻可用的。有一次我登录一台精简到不能再精简的老机器,上面连python都没有,就靠这段脚本把整个月的排期打印了出来,那一刻我真心觉得cal这个小命令值得按“实战工具”的标准重新认识一遍。
