给终端党的一份礼物:Linux 下的 cal 命令,多数人只在刚学时敲过两下,之后就被 GUI 日历替代了。我在服务器上排查日志时间线、写排班脚本、确认某个上线日期是否撞上周末时,反而越来越依赖它。它能快速输出当月/全年日历、按周起始日切换视图、用儒略日表示一年中的第几天,甚至能直接在终端里标出今天,适合运维、嵌入式开发、写 shell 脚本的朋友,也是面试题里经常冒出来的“冷门常用命令”。这篇文章我把 cal 的参数、跨平台差异和脚本化玩法拆开讲,顺手附上我踩过的坑。
1. 为什么值得在终端里专门学一个日历命令
1.1 一条命令回答“今天星期几,下周几号”
你当然可以用 date 看星期,但 cal 的价值不在单点,而在“全局视野”。登录服务器后敲一下 cal,当月布局直接铺在眼前:哪个日期落在周一,哪一周跨月,一眼就清楚。我经常配合 date 一起用:
bash复制cal && date "+%Y-%m-%d %A"
输出里本月日历和精确日期、星期并排,排期时对照非常直观。这个习惯在纯字符界面的服务器上和嵌入式 Linux 板子上尤其好用,没有 GUI 可开,终端即 UI。
cal 的默认输出是当月,首行是“月份 年份”,第二行是星期缩写,下面按行排日期。星期缩写和你当前 locale 相关,国内多数系统的 LANG=zh_CN.UTF-8 会显示“一 二 三 四 五 六 日”或者英文缩写,具体看发行版。想统一行为时,可以给命令加 LC_ALL=C cal,强制用 C locale,输出固定为英文缩写和标准 7 列宽度,脚本解析时不踩雷。
1.2 GNU cal 和 BSD cal:同一名字,两套脾气
这里必须先打预防针,否则你在 macOS 和 Linux 间切换时会被参数坑到。
Linux 发行版默认的 cal 来自 util-linux,参数丰富,支持 -A、-B、-w、--color、--highlight 等。而 BSD 系统(macOS、FreeBSD)上的 cal 来自 BSD 工具集,参数少,行为也不同。典型例子:BSD 的 cal -h 是“关闭今日高亮”,GNU 的 cal -h 则是打印帮助;GNU 支持 -A 2 显示后两个月,BSD 没有这个选项,只有 ncal 部分支持类功能。
所以写跨平台脚本时要小心,后面第 4 章我会给兼容性方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心参数拆解:从入门到进阶
2.1 月份、年份、月+年,三种基本姿势
cal 的通用格式是 cal [[[日] 月] 年],但实际最常用的是以下几种:
bash复制cal # 当月
cal 2025 # 2025 全年
cal 7 2025 # 2025 年 7 月
cal 07 2025 # 等价,月份可带前导零
这里有个经典参数坑:当你只写一个数字时,cal 会先判断它是不是 1 到 12,是则当作“月”,否则当作“年”。换句话说:
bash复制cal 12 # 显示当年 12 月
cal 13 # 显示 13 年全年
cal 2025 # 显示 2025 年全年
cal 25 会真的显示公元 25 年的日历,别在脚本里随手传一个两位数进去,很容易得到非预期输出。我之前见过有人写 cal $(date +%m) 想显示当月,逻辑上没错,但如果脚本在 13 月这种非法环境变量下跑,行为就会开始迷惑——好消息是现实不会,但校验月份依然是好习惯。
2.2 -y、-3、-1:切换视图范围
默认是当月,但排期常常需要看前后月,甚至整年:
bash复制cal -y # 当前年份全年
cal -3 # 上个月 + 本月 + 下个月
cal -1 # 只显示本月,等价于默认
cal -A 2 # 在当月后面多显示 2 个月
cal -B 1 # 在当月前面多显示 1 个月
cal -A 2 -B 2 # 前后各多显示 2 个月
-3 是我个人使用频率最高的参数。看排期时,前后月的边界情况(比如月底是否撞周末)比只看当月清楚得多。-A 和 -B 则适合那种“从现在起往后排 5 周”的场景,比如迭代排期。
注意 -A、-B 是 GNU 扩展,BSD 不支持。macOS 用户想达到类似效果,可以用 ncal -A 2 -B 2(在装有 ncal 的前提下),或者退而求其次 ncal -3。
2.3 -m:让周一成为一周的第一天
国内团队排期,习惯上一周从周一开始。GNU cal 默认按周日开头,如果你看不惯,用 -m 切换:
bash复制cal -m # 周一作为每周第一天
cal -3 -m # 组合使用,最常见
后端写定时任务时,周一开头和周日开头不仅影响阅读,还会影响你肉眼判断“周数”的正确性。我曾因为默认周日视图,把某个每周日凌晨的 cron 任务看错了一天归属,调试了很久才发现问题不在 cron,而在我看日历的方式。现在服务器上我直接用 alias 固定为周一视图。
BSD 的 ncal 也有 -m,它表示周一开头。但 BSD 的 cal 本身对 Monday 开头的支持较弱,更多依赖系统 locale 设置,这点在 macOS 上不少人中过招。
2.4 -j:儒略日输出,脚本里的大杀器
-j 让 cal 用“一年中的第几天”来显示日期,范围 1 到 365/366。比如:
bash复制cal -j # 当月,按儒略日显示
cal -j 2025 # 全年按儒略日显示
cal -j 7 2025 # 2025 年 7 月按儒略日显示
儒略日的价值在于拿到一个单调递增的数字,天然适合算跨天周期。比如某个保质期任务,需要知道“从 1 月 1 日到某天过去了多少天”,cal -j 直接在日历上给你标号,比 date -d "..." +%j 更直观,因为它把全年每一天的序号都摊开了。两者配合使用,写脚本时有对照:
bash复制date +%j # 今天是一年中的第几天
cal -j | tail -n +3 # 当月每日对应的儒略日
我记得在做一个按季度巡检的脚本时,直接用 cal -j "$year" 生成全年序号表,再去掉周末后做工作日清单,比每次循环调 date 快得多,逻辑也简单很多。
2.5 周数、高亮、颜色:容易被忽略的现代功能
新版 util-linux(2.36+)给 cal 加了不少实用选项:
bash复制cal -w # 显示 ISO 周数
cal --highlight=today # 高亮今天
cal --color=always # 强制开启颜色输出
cal -b # 今天用粗体显示
-w 在每行左侧或右侧标出 ISO 周号,具体位置因版本而异,但对周报、迭代管理很有用。--highlight=today 则直接把今天用反色/颜色标出,实测在直观性上比旧版强很多。如果你用的是旧发行版,也没有关系,第 3 章我会给一个纯 grep/sed 的高亮方案。
顺带一提,util-linux 的 cal 还支持环境变量 CAL 和 CAL_BOLD 来控制配色,例如:
bash复制export CAL="\e[1;35m" CAL_BOLD="\e[1;31m"
cal --color=always --highlight=today
这个玩法适合把日历做成自己专属风格,但要注意终端是否支持 ANSI 颜色,SSH 到老设备上时可能会显示乱码,建议只在交互 shell 里启用。
3. 让 cal 真正干活:脚本化与自动化场景
3.1 高亮今天:没有新版参数也能做
如果系统上的 cal 不支持 --highlight,用 grep 也能实现同样的效果:
bash复制today=$(date +%e | tr -d ' ')
cal | grep --color=always -E "(^|[[:space:]])${today}([[:space:]]|$)"
这里的关键坑是:date +%e 可能带前导空格(1 号是 1 而不是 1),所以先 tr -d ' ' 去掉。然后给日期数字加边界条件 (^|[[:space:]]) 和 ([[:space:]]|$),防止匹配到其他数字的局部。比如今天 5 号,不能把 15、25、5 月都糊里糊涂高亮。
要想完整高亮整行,可以升级成函数放进 .bashrc:
bash复制calhl() {
local d=$(date +%e | tr -d ' ')
cal "$@" | sed "s/ ${d} / [${d}] /"
}
这个函数把所有日期参数原样透传给 cal,你写 calhl -3 也能用。注意 sed 替换的是空格包裹的日期,对 1 到 9 号的日期格式是“空格数字空格”,替换后变成 [ 5] 这样带括号的形式,视觉上已经足够。
3.2 用 cal 生成月度排期表头
在写排期文档、巡检日志、月报时,cal 可以直接当作表头用:
bash复制year=$(date +%Y)
month=$(date +%m)
cal "$month" "$year" > monthly_cal_${year}${month}.txt
结合 awk 还能把备注加进去。举个例子,我想在每月 15 号后面标记“对账日”:
bash复制cal | awk '{
for (i=1; i<=NF; i++) {
if ($i == 15) { $i = $i "*" }
}
print
}'
这样输出 15* 就代表对账日。这个思路可以扩展成简单的排期日历:某几个日期加星号、加备注,用 awk 处理输出即可。我实际写月报时,是先把当月 cal 输出存成文本,再用注释文本拼接,效果比表格自由得多。
3.3 用 cal 判断某月有多少天
判断月末日期是个很常见的需求,很多人第一反应是 date -d "next month -1 day",但 cal 也能做,而且跨平台时更稳:
bash复制get_last_day() {
local m=$1 y=$2
cal "$m" "$y" | awk 'NR>2 {for(i=1;i<=NF;i++) if($i+0>max) max=$i+0} END{print max}'
}
get_last_day 2 2024 # 输出 29
get_last_day 2 2025 # 输出 28
这行 awk 的逻辑是:跳过前两行(月份行和星期缩写行),从日期行里找出最大数字,那就是当月最后一天。处理带前导空格的日期时 $i+0 会把它转成纯数字,所以 1 号这样的也没问题。
配合 date 验证一下:
bash复制date -d "2024-02-01 +1 month -1 day" +%d # 29
两个结果能对上是正确性的好证明。实际写脚本时,我推荐用 date 做核心判断,cal 做展示和交互验证,各取所长。
3.4 判断某天星期几:cal 的排版价值
虽然 date -d "2025-07-20" +%A 能直接告诉你是星期几,但如果要一次性查看整个月所有周末的位置,cal 的排版价值就出来了:
bash复制cal 7 2025 | awk 'NR==3 {print "第一周的周末列:", $1, $7}'
配合周视图,你还能快速回答“这个月的第三个周三是几号”。方法很简单:找到第三周的行,数第 3 列(周一开头时)或第 4 列(周日开头时)。GNU cal 默认周日开头且没有 -m 时,周三在第四列;加上 -m 后,周三在第三列。这个肉眼定位速度比打开日历软件快得多。
3.5 校验参数合法性的小技巧
cal 对非法输入会直接报错退出,利用这一点可以写参数校验:
bash复制month=13
if cal "$month" 2025 >/dev/null 2>&1; then
echo "month $month 合法"
else
echo "month $month 非法"
fi
注意:cal 13(只有一个参数)不会报错,因为它被当作年份“13”看待;但 cal 13 2025 这种“月 年”两参数形式会报非法月份。所以校验月份时要显式传两个参数,不然校验等于白做。
4. 常见问题、坑点与排查实录
4.1 执行 cal 提示 command not found
最小化容器、某些精简版发行版或 BusyBox 环境里,可能没有 cal。Debian/Ubuntu 上它属于 util-linux,一般默认安装,但被裁剪过镜像可能没有:
bash复制apt install util-linux
CentOS/RHEL 系:
bash复制yum install util-linux
如果发行版提供 bsdmainutils,也能装上三合一版的 cal/ncal。装完验证:
bash复制which cal
cal --version # GNU 版本
cal -h # BSD 版本,会打印帮助或关闭高亮
这里有个容易混淆的点:cal -h 在 GNU 下是帮助,在 BSD 下是“关闭今日高亮”。所以写脚本时不要依赖 cal -h 来探测功能,建议用 cal --version 或直接测某个 GNU 扩展参数是否存在。
4.2 输出错位、中文乱码、月份不齐
在 LANG=zh_CN.UTF-8 的终端里,cal 的星期标题可能是“日 一 二 三 四 五 六”“一 二 三 四 五 六 日”之类,有些中文 locale 下全角字符宽度会打乱对齐。我碰到过一种情况:同一台服务器,locale 是 en_US 时日历整齐,切到 zh_CN 后整个表格向右偏移,输出到日志文件里完全没法看。
解决办法,输出到文件或脚本解析时统一强制 C locale:
bash复制LC_ALL=C cal -3
LC_TIME=en_US.UTF-8 cal -3
LC_ALL=C 效果最干净,缺点是星期标题变成英文缩写(Sun Mon Tue...),但换来宽度稳定、解析安全。在脚本里我通常这样设:
bash复制export LC_ALL=C
这不仅是 cal 的问题,很多依赖固定文本宽度解析的命令都建议这么干。日历显示类工具最大的敌人,从来不是功能缺失,而是 locale 的隐性影响。
4.3 GNU 和 BSD 参数不兼容的避坑
跨平台脚本最怕一个写法两边行为不一样。我列一张常用参数对比表:
| 需求 | GNU cal (util-linux) | BSD cal / ncal |
|---|---|---|
| 显示前后月份 | cal -3 |
ncal -3 |
| 前后加 N 月 | cal -A 2 -B 1 |
ncal 部分版本支持 |
| 周一作为首列 | cal -m |
ncal -m |
| 显示周数 | cal -w |
ncal -w |
| 显示儒略日 | cal -j |
cal -j / ncal -j |
| 高亮今天 | cal --highlight=today |
cal -h 默认高亮,cal -h 关闭 |
| 显示帮助 | cal -h / --help |
man cal |
从这个表能看出,BSD 上如果你想要 GNU 的灵活度,几乎都得用到 ncal。所以我的跨平台建议很直接:脚本里优先检测有没有 ncal,有就走 ncal 参数;没有再用 GNU 参数。检测脚本可以写成:
bash复制if command -v ncal >/dev/null 2>&1; then
cal='ncal -b' # ncal 的 -b 会输出经典 cal 风格
else
cal='cal'
fi
ncal -b 输出横向日历布局,视觉上和 GNU cal 接近,适合做默认 fallback。别让脚本在两套参数之间反复横跳,那是灾难的开始。
4.4 为什么 cal 2025 7 会报错或表现怪异
cal 的参数顺序是“月 年”,不是“年 月”。所以:
bash复制cal 7 2025 # 正确,2025 年 7 月
cal 2025 7 # 错误,会被解析为“月份 2025”,直接报非法月份
我见过不少从 date 习惯带过来的写法把 cal $year $month 写反,结果脚本在年初跑的时候永远报错。调试时第一件事就是确认参数顺序,也可以养成习惯写成:
bash复制cal "$(date +%-m)" "$(date +%Y)"
这种写法即使月是 1、7、12,也能被正确识别。还有一个隐藏点:cal 07 2025 可以,cal 7 2025 也可以,但 cal 007 2025 这种带前导零的三位数会进入“年”判断分支,行为会不一样,脚本里建议统一用 %-m 去掉前导零。
4.5 高亮正则在 grep 里匹配到多余数字
前面提到的高亮函数,正则是 (^|[[:space:]])${today}([[:space:]]|$)。如果省略边界符号,直接用 grep "$(date +%d)",你会发现 5 号时会同时亮起 5、15、25、甚至 5 月,输出面目全非。这是我第一次写高亮脚本就踩过的坑,后来总结两条规则:
- 用
%e而不是%d,因为%d会给 1-9 补前导零,cal 输出里并没有前导零,永远匹配不上。 - 必须做边界匹配,日期数字前后在 cal 输出里一定是空格或行首行尾。
多强调一句:date +%e 在脚本里最好 tr -d ' ' 清理,因为标准输出可能是 " 5",带着空格参与正则容易出事。
5. 把 cal 玩出花的组合技巧
5.1 alias 与 shell 函数:定制日常视图
我的 .bashrc 里常年躺着这三行:
bash复制alias cal3='cal -3 -m'
alias caly='cal -y -m'
alias caljl='cal -j'
cal3 是我最常用的,看前后三个月,周一开头;caly 年初做计划时看全年;caljl 看儒略日,配合 date +%j 做跨日计算。
再配一个高亮今天的函数,日常打开终端就能看到当前日期在日历上的位置:
bash复制calt() {
local d=$(date +%e | tr -d ' ')
if cal --highlight=today >/dev/null 2>&1; then
cal -3 -m --highlight=today "$@"
else
cal -3 -m | sed "s/ ${d} / [${d}] /"
fi
}
函数思路是先探测新版参数是否可用,能用就走原生高亮,不能用就 sed 兜底。这个“探测后降级”的写法,在跨服务器维护场景里非常实用,比固定参数硬闯要稳。
5.2 cal 和 date、awk、grep 的复合命令
cal 单用是查询工具,和 date/awk 一组合就变成小计算器。举几个我实际用过的:
找出某个月第三周的周三:
bash复制cal -m 7 2025 | awk 'NR==4 {print "第三周周三:", $3}'
前提是 -m 让周一开始,周一在第 1 列,周三就在第 3 列。NR 行号需要根据实际输出微调,因为第一行是月份标题、第二行是星期标题、第三行开始才是日期。多测几行就知道规律了。
计算当月已过去的天数:
bash复制echo $(( $(date +%d) - 1 ))
这个不看 cal 也能算,但用 cal -j 能直接看到今天是第几天,再减去月初儒略日就是已过天数:
bash复制today_j=$(date +%j)
first_j=$(cal -j | awk 'NR==3 {print $1}')
echo "本月已过 $(( today_j - first_j + 1 )) 天"
这些组合命令并不复杂,核心思想是把 cal 的输出当成文本表格来解析,而不是当成“日历软件”来用。一旦你把它当作一个数据源,能组合出的玩法就多起来了。
5.3 ncal:另一个被低估的选择
ncal 是 BSD 工具包里的日历命令,和 cal 互补。它默认竖排显示,一周一行,某些场景比横向布局更好读:
bash复制ncal # 竖排视图
ncal -m 7 2025 # 指定月份,周一开头
ncal -w 2025 # 带周数
ncal -e 2025 # 显示复活节日期(对欧美排期有点用)
ncal -e 能直接告诉你复活节是几月几号,这个信息结合值班表、节假日安排时非常省事。GNU cal 没有这个功能。
如果发行版没有 ncal,装一下 bsdmainutils 就能用。Debian/Ubuntu 上:
bash复制apt install bsdmainutils
装完 cal 和 ncal 并存,互相补充。我个人的经验是:交互查看排期用 cal -3 -m,竖排查看每周结构时用 ncal,写脚本处理日期逻辑时用 date,三者各司其职,不要试图用一个命令包办所有事。
最后再分享一个我自己的使用习惯:不要在脚本里死记 cal 的版本,而是启动时做一次功能探测,后续统一走探测出的参数组合。看似多写了两行,但换来的是五年内不翻车。cal 这个命令虽然低调,能做的事比想象中多,把它的参数吃透,终端里的日期管理会比 GUI 日历顺手得多。
