openEuler登录界面定制与Shell脚本版权头规范实战 在 openEuler 上折腾 Shell 编程登录界面和脚本头部版权信息这两件事是我个人认为性价比最高的两个小改造一个每天上线都能看到一个能让你的脚本在三个月后还看得懂。我最初接触 openEuler 时每次 ssh 上去看到的都是系统默认那几行提示后来花了一个下午把 issue、motd、profile.d 的关系梳理清楚顺手用 figlet 做了一套带实时 IP 和系统负载的登录欢迎界面与此同时因为写的 Shell 脚本越来越多也开始给每个新脚本补上规范的版权头和版本记录再回头整理老脚本时真心感慨头部信息这东西早写早省心。这篇文章就围绕这两块展开。前半部分讲清楚 openEuler 登录界面的完整链路然后带着你从装工具、改配置到最终落地一步步操作后半部分给出一套可以直接抄走的 Shell 脚本头模板以及批量给历史脚本补版权头的实用方案。适合刚入门的运维和 Linux 爱好者照着做也适合写过不少脚本但一直没注意过可维护性的老手做一次体检。1. 先想明白登录界面的背后是什么1.1 从按回车到看见界面登录过程中的关键节点很多人以为登录界面就是开机后看到的那一屏改起来无非是换个背景图或者改一行文本。但真实情况比这复杂得多我在 openEuler 上折腾时第一步就是把整个登录流程拆开看了一遍。当你按下回车或者通过 ssh 连上服务器系统实际上会经历几个阶段登录前控制台由 systemd 的 getty 服务管理agetty 在显示登录提示符之前会读取 /etc/issue 文件并把内容显示在屏幕上。这就是你登录前看到的系统标识、内核版本那些信息。登录认证输入用户名密码后由 login 或者 sshd 调用 PAM 模块完成认证。PAM 在这一阶段还负责触发 motd 的显示。登录后认证成功PAM 读取 /etc/motd 文件并显示公告信息随后启动用户的登录 Shell。Bash 读取 /etc/profile再依次加载 /etc/profile.d/ 下的所有 .sh 脚本。Shell 初始化如果是交互式登录Bash 还会继续读 ~/.bash_profile 等用户级配置。把这条链路画出来之后你会发现一个关键结论openEuler 的登录界面不是一个文件决定的而是 /etc/issue、/etc/motd、/etc/profile.d/ 三个位置叠加出来的结果。想做出一个好看又实用的登录界面不是改一个文件就行而是要想清楚每一层承担什么职责。1.2 issue、motd、profile.d 三层分工别再傻傻分不清我第一次直接把一堆字符画塞进 /etc/issue结果登录后完全没有显示后来才明白自己搞错了时机。这三个文件的分工其实非常清晰配置位置显示时机推荐用途典型内容/etc/issue登录前仅限控制台 / 串口终端系统身份标识、简短标语、登录前通知主机名、内核版本、日期时间/etc/motd登录成功后PAM 触发公告、维护窗口提醒、联系 SLA 负责人停机维护通知、巡检提醒、注意事项/etc/profile.d/*.sh登录 Shell 启动时执行动态信息面板实时 IP、系统负载、磁盘剩余、当前用户我理解这三个角色可以类比成前台迎宾、签到台公告栏和入职培训。issue 是用户还没进门时看到的招牌motd 是进门之后贴在墙上的通知profile.d 则是你坐下之后跟你交代的今日情况。有一个常见的误解是改了 /etc/issue 就能让 ssh 登录前显示自定义内容实际上 ssh 协议层面根本不会读取 /etc/issue它走的是 sshd_config 里的 Banner 配置。这个区别后面会专门讲。1.3 控制台登录界面和图形登录界面是两条线需要先泼一盆冷水本文讲的是纯字符控制台和远程 ssh 场景下的登录界面。如果你在 openEuler 上装了图形桌面看到的 GDM 登录界面走的是另一套机制跟 /etc/issue、/etc/motd 完全无关我见过有人在图形登录界面卡住时去改 /etc/issue折腾半天毫无变化就是这个原因。另外补充一句我在 ARM 架构的 openEuler 服务器上也验证过这套方案包括跑在 libvirt/KVM 虚拟机里的场景字符画和 Shell 脚本都是纯文本处理和 CPU 架构无关鲲鹏、飞腾上都能直接用。不过如果是 ARM 虚拟机登录时执行 figlet 这类工具会有一定的性能开销后面章节会专门讲怎么优化避免每次登录都卡一下。2. 手把手构建自定义登录界面从安装工具到效果落地2.1 准备字符画工具figlet 和 toilet 的安装与用法字符画可以说是登录界面的灵魂。手写 ASCII Art 太痛苦推荐直接用工具生成。openEuler 的默认源里就有这两个明星工具dnf install -y figlet toilet如果你的机器配置的是 yum 源也可以直接用 yum install 安装openEuler 向下兼容这条命令。离线环境装不了的话可以直接访问 patorjk.com/software/taag 这类在线字符画生成网站把生成的纯文本粘贴到配置文件里效果一样。figlet 的基础用法非常简单figlet Welcome figlet -f slant Hello openEuler其中 -f 参数指定字体我常用的是 standard、slant、big 和 banner各有各的气质。toilet 可以理解为 figlet 的增强版支持渐变色彩toilet --metal openEuler在 tty 里用 toilet 的效果比 figlet 更炫但要注意部分老终端对 ANSI 色彩的支持不稳定公开展示时反而可能乱码生产环境我更推荐老老实实用 figlet 的纯文本输出。安装完成后先用一句 figlet 测试一下终端宽度心里大概有个数避免后面排版翻车。2.2 修改 /etc/issue登录前的静态迎宾墙先备份再修改这是最基本的纪律cp /etc/issue /etc/issue.bak然后用 vim 编辑 /etc/issue比如我当年写的第一版\S 欢迎访问 \n 内核版本: \r 当前时间: \d \t 当前终端: \l这里有一张 agetty 支持的转义符速查表建议收藏转义符含义示例输出\S操作系统名称openEuler\n主机名node01\r内核版本5.10.0-60.18.0.50.oe2203\m硬件架构aarch64 / x86_64\d日期2025-03-16\t时间10:24:33\l当前终端名tty1\u当前在线用户数2\eESC 转义字符用于颜色控制配合 ANSI 序列值得留意的是\e 配合 ANSI 颜色码可以给 issue 加上颜色例如\e[1;32m\S\e[0m但这条我在生产机器上踩过坑。部分远程管理卡和串口终端并不完整支持 ANSI 颜色显示出来的就是一串乱码。所以生产环境我最终选择了去掉颜色只保留纯文本。如果想要更复杂的字符画可以把 figlet 的输出直接粘贴进 issue。注意一个关键点/etc/issue 是静态文件里面不能写命令figlet 必须在本地生成好再贴进去。我在网络热词里看到有人问openeuler 登录界面怎么改十有八九就是卡在这一步——以为文件里能放命令结果把整段回显当文本输出。2.3 动态信息面板用 /etc/profile.d 打造登录后的实时欢迎页静态 issue 有个硬伤它无法显示实时 IP、负载这些会变的信息。所以真正承载动态信息的重担落在 /etc/profile.d 上。直接看方案我建议新建一个脚本而不是改 /etc/motdcat /etc/profile.d/login-info.sh EOF #!/usr/bin/env bash # 登录后动态信息面板 - openEuler / Linux 通用 if [ -n $PS1 ] [ -z $LOGIN_INFO_SHOWN ]; then export LOGIN_INFO_SHOWN1 echo if [ -x /usr/bin/figlet ]; then figlet -f standard $(hostname -s) fi echo echo 登录时间 : $(date %F %T) echo 登录用户 : $(whoami) echo 服务器IP : $(hostname -I | awk {print $1}) echo 系统负载 : $(uptime | sed s/.*load average: //) echo echo fi EOF chmod x /etc/profile.d/login-info.sh这个脚本里的每个判断都不是多余的我展开说一下[ -n $PS1 ]判断交互式 Shell这是整段脚本最关键的防护。如果不加这个判断所有非交互式连接都会被污染。比如ssh host ls这种远程执行命令的场景如果脚本直接 echo 一堆东西会导致传递的管道数据被这些字符干扰scp、rsync、自动化采集全都可能出诡异问题。加上这个判断之后只有交互式登录的 Shell 才会显示面板。[ -z $LOGIN_INFO_SHOWN ]防止重复显示虽然 /etc/profile.d 的脚本在普通的非登录 Shell 中不会重复加载但同一会话内再次登录或者嵌套 su 时这个变量能保证面板只出现一次。hostname -I | awk {print $1}取第一个 IP服务器多网卡时 hostname -I 会输出一串地址用 awk 取第一个往往是最核心的那个业务 IP比直接甩三个地址出来干净得多。load average 的提取uptime 输出的负载信息在最后一段用 sed 把前面的文字删掉得到的就是 0.00, 0.05, 0.10 这种格式提醒你关注 1、5、15 分钟的负载趋势。有一点需要明确我没有用 /etc/motd 做动态面板。虽然网上很多教程教你每次登录前更新 motd但 motd 由 PAM 管理容易在并发登录时产生竞争而且多用户同时登录会反复刷文件。profile.d 里的 echo 是即时输出不落盘没有这些问题。2.4 给 SSH 远程登录单独加一道 Banner如果你希望远程用户输密码之前就看到一段提示比如该服务器仅限授权访问那就要用 sshd 的 Banner 配置而不是 /etc/issue。echo /etc/ssh_banner echo 注意该服务器仅限授权用户访问 /etc/ssh_banner echo 所有操作将被记录非法登录将追责 /etc/ssh_banner echo /etc/ssh_banner echo Banner /etc/ssh_banner /etc/ssh/sshd_config systemctl restart sshd这里有两个很容易踩的坑第一Banner 文件不支持转义符展开。你在 /etc/ssh_banner 里写 \n、\d 这些字符它只会原样输出不会变成换行或日期。所以 Banner 里不要依赖动态信息写死静态提示文本就好。第二如果你之前源码编译升级过 OpenSSH配置文件的路径可能不是 /etc/ssh/sshd_config编译安装时如果指定了 prefix配置文件会落在 /usr/local/etc 或者自定义目录下。改了配置文件发现不生效时先确认 sshd 实际读取的是哪个文件sshd -T | grep banner可以快速验证当前生效配置。本地登录和远程登录区分开之后整体效果就是本地控制台登录前看到 issue 迎宾ssh 登录前看到 Banner 警告登录成功后看到动态信息面板。三个模块各自独立互不干扰维护起来非常清晰。3. Shell 脚本头部版权信息规范、模板与批量落地3.1 为什么脚本头值得认真写说完了登录界面切换到 Shell 编程这边的高频话题脚本头部的版权信息。我在 openEuler 上写脚本踩过的最大坑不是语法错误而是三个月后回来看自己写的脚本完全想不起来这个脚本是干嘛的、执行时需要传什么参数、用了哪些外部命令。运维脚本尤其严重——平时就一两百行感觉没必要写注释等线上出了故障要回滚时全靠抓瞎。脚本头的价值我总结成三句话对自己下次接手时快速恢复上下文不用把整个脚本读一遍才知道它是干什么的。对团队别人接手脚本时知道找谁遇到 Bug 能通过版本记录定位是哪个改动引入的。对开源/合规如果脚本要放到公司内部共享或者对外开源明确的版本号、版权声明和许可证是基本素养。openEuler 生态里很多项目用的是木兰宽松许可证 Mulan PSL v2这也是国内开源社区最常见的选择之一。我见过不少团队抱怨脚本没有文档但脚本头的规范其实就是轻量级的文档成本极低回报极高。3.2 一套可以直接抄走的脚本头模板下面这份模板我在 openEuler 20.03、22.03 上实测过也兼容其他主流发行版可以直接抄走#!/usr/bin/env bash # # 脚本名称: backup_logs.sh # 功能描述: 归档并清理指定目录下的日志文件保留最近 N 天 # 适用平台: openEuler 20.03/22.03, 兼容 CentOS 7 # # 作者 : 你的名字 # 邮箱 : your_nameexample.com # 创建日期: 2025-03-16 # 版本 : v1.1.0 # # 修改记录: # v1.1.0 2025-03-16 增加磁盘空间不足时的自动告警 # v1.0.0 2025-03-01 初始版本 # # 版权声明: Copyright (c) 2025 your_name # 许可证 : MulanPSL-2.0 (https://license.coscl.org.cn/MulanPSL2) # # 使用示例: ./backup_logs.sh -d /var/log/app -k 7 # 参数说明: # -d 目录 要处理的日志目录 # -k 天数 保留最近 N 天的文件 # -h 显示帮助信息 # set -euo pipefail模板里有几个细节值得专门解释第一#!/usr/bin/env bash比#!/bin/bash更灵活。它通过 PATH 查找 bash在有些自编译 bash 或者特殊环境里能避免路径写死的问题。缺点是如果 env 的 PATH 被篡改可能加载到错误的 bash所以安全敏感场景我反而会写死#!/bin/bash两者各有取舍。第二set -euo pipefail不是头部装饰是脚本的保命符。-e让脚本在遇到错误时立刻退出-u让变量未定义时直接报错pipefail让管道中任何一个命令失败都被捕捉。这三件套能让脚本避免很多隐性错误。不过要注意它的副作用如果脚本的某个命令失败是可预期的比如 grep 找不到内容就要配合|| true或者if判断否则脚本会异常退出。第三修改记录不要偷懒。哪怕只是改了一行也花十秒钟追加一条记录。版本号遵循 v主版本.次版本.修订号 的规则主版本是重大重构次版本是新增功能兼容旧逻辑修订号是修复 Bug。3.3 可执行脚本与函数库的头部差异很多人没意识到可执行脚本和要被 source 加载的函数库头部写法应该不一样。可执行脚本有 shebang可以直接./script.sh执行。但函数库不一样函数库的定位是被其他脚本 load 进来复用它本身不应该有执行入口也不能直接定义 shebang强行加set -euo pipefail还会破坏调用方的环境——因为函数库被 source 时这些设置会直接作用于调用方 Shell可能导致调用方脚本的行为跟着改变。函数库的头部模板应该是这样# # 函数库名称: lib/common.sh # 功能描述: 提供日志输出、文件锁定、配置解析等公共函数 # # 作者 : your_name # 创建日期: 2025-03-16 # 版本 : v1.0.0 # # 使用方式: 在脚本中通过 source lib/common.sh 加载 # 注意 : 本文件不直接执行不设置 set -euo pipefail # # 版权声明: Copyright (c) 2025 your_name # 许可证 : MulanPSL-2.0 #函数库头部和可执行脚本头部最大的区别就是它必须明确写出本文件不直接执行并且用醒目注释提醒使用者不要在里面设置影响全局的 Shell 选项。我在团队里见过排查了两小时的 Bug最后发现是公共函数库里开了set -u导致加载它的旧脚本里所有未定义变量全部报错。这个教训记忆犹新。3.4 批量给历史脚本补版权头的实战脚本有规范的头部模板之后最头疼的事情来了以前写的几十个脚本都没有头难道要一个个手动打开粘贴当然不是。我写了一个批量补头脚本用到了 Shell 编程里非常经典的 for 循环、shift 参数处理和文件操作正好也呼应了前面聊的循环语法#!/usr/bin/env bash set -euo pipefail # 批量给 Shell 脚本添加版本号和版权头 # 用法: ./add_header.sh -f ./scripts/*.sh -t ./header.tpl usage() { echo 用法: $0 -f 文件列表 -t 模板文件 echo 示例: $0 -f \./scripts/*.sh\ -t ./header.tpl exit 1 } files tpl while getopts f:t:h opt; do case $opt in f) files$OPTARG ;; t) tpl$OPTARG ;; h) usage ;; *) usage ;; esac done shift $((OPTIND - 1)) if [ -z $files ] || [ -z $tpl ]; then usage fi for f in $files; do if [ ! -f $f ]; then echo 跳过非文件: $f continue fi # 已有版权头的文件不再重复添加 if grep -q Copyright $f 2/dev/null; then echo 已包含版权头, 跳过: $f continue fi # 保留原 shebang, 把头部模板插到第一行之后 if head -1 $f | grep -q ^#!; then awk NR1 {print; print ; while ((getline line $tpl) 0) print line; fflush(); next} {print} \ $f $f.tmp mv $f.tmp $f else cat $tpl $f $f.tmp mv $f.tmp $f fi chmod x $f echo 已添加头部: $f done这段脚本的操作要点getopts解析命令行参数然后shift $((OPTIND - 1))把选项部分剥离掉这是标准姿势很多人在这一步容易漏掉导致后面处理位置参数时多出莫名其妙的内容。通过head -1检查文件是否已有 shebang。如果有用 awk 在保留第一行的前提下插入模板避免把 shebang 顶到第二行。用grep -q Copyright做幂等判断确保脚本跑第二次不会给同一个文件重复叠加头部。chmod x 是顺手帮你把缺失的执行权限补上如果你不需要可以删掉。如果你只有一个文件要补头也可以直接手动用 sed 在 shebang 后面插入sed -i 2i\ # 功能描述: 这是手动补头的脚本\ # 作者: your_name\ # 许可证: MulanPSL-2.0 target.sh两个方案都实测可用看你的场景选择。批量文件用脚本单个文件用 sed 更快。4. 常见问题与排查实录登录界面最容易翻车的 5 个场景4.1 改了 issue 但就是不显示新内容这是我在 openEuler 上被问过最多的问题。排查路径按顺序来第一步确认你是从哪个入口登录的。如果是 ssh 登录issue 本来就不会显示它只属于控制台/串口终端这一点前面已经强调过。第二步确认 agetty 没有被加 -i 参数。-i 参数会忽略 issue 文件直接显示默认提示符可以用这条命令检查运行中的 getty 进程ps aux | grep agetty如果启动命令里带了 -i通常出现在 /etc/systemd/system/gettytty1.service.d/ 目录下的 override 配置里删掉或注释掉后systemctl daemon-reload systemctl restart gettytty1即可。第三步检查你打开的是不是 tty1。子控制台 tty2~tty6 各自有独立的 getty 服务它们也读 /etc/issue但如果文件是改在特殊路径或者你有多个 issue 副本容易改错文件。用ls -l /etc/issue*看看有没有异常。还有一种隐蔽情况你做完修改后没有切出当前 tty 再切回来。agetty 只在服务启动时读取一次 issue已经运行中的终端不会实时刷新。最简单的验证方式是按 CtrlAltF2 切到另一个 tty如果那边显示的是新内容说明配置没有问题。4.2 字符画排版错乱、被折成乱麻字符画最大的敌人是终端宽度。figlet 默认按 80 列输出但你的终端窗口宽度可能是 120 列或者 60 列输出结果就会出现两种情况太窄导致自动折行或者太宽导致右侧大片留白。解决方法在生成字符画时直接指定宽度或者在 profile.d 脚本里用当前终端宽度动态选择字体字号。我用的方案是if [ ${COLUMNS:-80} -lt 100 ]; then figlet -f small $(hostname -s) else figlet -f standard $(hostname -s) fi其中 COLUMNS 变量是 Bash 在交互式终端下自动维护的列数当它小于 100 时使用更窄的 small 字体否则用 standard。这样无论你是窄窗口还是宽屏显示器登录面板都不会乱。还有一个容易被忽略的场景通过 PuTTY、MobaXterm 之类软件连接时不同终端的字体宽度不一致同样的字符画在不同软件里呈现的宽度差距很大。建议在确认排版时同时用两种不同的终端软件测试不要只在自己常用的终端里看到好看就收工。4.3 中文乱码与转义符失灵在 tty 和控制台场景下中文字符的显示经常翻车。尤其是 /etc/issue 里的中文在系统编码不是 UTF-8、或者终端字体不支持中文渲染时会出现满屏的方块。最稳妥的解决方案是登录界面这类面向终端的场景全部使用英文和纯 ASCII 字符画中文内容放到 motd 或 profile.d 里用 echo 输出。即便如此echo 中文时也建议先确认/etc/locale.conf里的 LANG 设置为 en_US.UTF-8 或 zh_CN.UTF-8否则 bash 的默认编码和终端编码不一致照样乱码。转义符失灵的问题则要区分场景。agetty 的 issue 转义符只在本地控制台生效如果你是通过 ssh 登录看到的默认提示符根本不走 agetty当然不会展开。另外有些微型终端工具串口终端、老式 KVM Over IP对转义符的支持不完整我在 ARM 服务器的带外管理调试时经常遇到这种情况表现为\n原样输出而不是主机名。这种场景就别折腾转义了直接用静态文本最安全。4.4 motd 重复显示或干脆不显示motd 的显示由 PAM 的 pam_motd 模块控制。在 openEuler 上/etc/pam.d/login 和 /etc/pam.d/sshd 里通常都有对 pam_motd.so 的调用。如果 motd 重复显示了通常是 PAM 配置里重复调用导致的看看这两个文件里有没有多余的 session optional pam_motd.so 行。如果 motd 完全不显示重点检查 /etc/motd 是否为空或权限异常ls -l /etc/motd cat /etc/motd文件权限需要让所有用户可读运行时如果被某个服务的 SELinux 策略挡住可以看审计日志ausearch -m avc -ts recent | grep motd还有一点openEuler 上如果装了 update-motd 相关组件会生成动态 motd这时候 /etc/motd 本身可能被符号链接或者由脚本动态改写静态写入的内容会被覆盖。检查ls -l /etc/motd时看到..结尾的链接说明是动态 motd 场景就要去对应目录排查。这也是为什么我前面更推荐用 profile.d因为它完全不碰 motd 这条链路省掉一半问题。4.5 登录过程明显变卡顿登录时卡顿最常见的元凶就是 profile.d 脚本里执行了重量级命令。我在 ARM 虚拟机里用 figlet 时踩到过这个坑鲲鹏这类 ARM 实例本身单核性能有限figlet 生成大段字符画加上 hostname 系统调用登录瞬间要停顿一两秒。优化手段按性价比排序优先把动态信息放在一起减少外部命令调用次数。比如date、whoami、uptime都是进程级调用每个都会产生 fork 开销能合并的尽量合并。对 figlet 做缓存。把字符画结果生成一次存到 /tmp 或者 /var/cache下次登录直接 cat 缓存文件避免每次重新计算。脚本里加一个判断if [ ! -f /var/cache/login-figlet.txt ] || [ $(( $(date %s) - $(stat -c %Y /var/cache/login-figlet.txt) )) -gt 86400 ]; then figlet -f standard $(hostname -s) /var/cache/login-figlet.txt fi cat /var/cache/login-figlet.txt这样只在缓存过期或主机名变化时重新计算平时登录零开销。不要在 profile.d 里执行需要网络请求或者磁盘扫描的命令。有人喜欢在登录时显示公网 IP于是脚本里放了一个 curl 请求结果每次登录都要等好几秒超时。动态信息面板里只放本机能秒出的信息需要外网请求的内容放到用户手动执行的独立脚本里。登录口令验证、文件系统加载这种延迟通常不在 profile.d 的管辖范围如果连输入用户名密码都卡建议优先检查系统负载和磁盘 I/O而不是纠结脚本。5. 把这套操作收拢成一个维护脚本文章快到最后分享一个我实际工作中的小技巧把上面所有登录界面的配置动作收拢成一个维护脚本放到 /usr/local/bin/update-login-banner.sh 里。以后想改任何一处跑一条命令就行不用每次对着文章重新回忆配置文件在哪。这个维护脚本的思路很简单就是把前面所有操作封装成函数加一个 case 分发参数。比如update-login-banner.sh issue只更新 issueupdate-login-banner.sh banner 新告警内容更新 ssh Bannerupdate-login-banner.sh panel更新 profile.d 的动态面板。每个函数内部先备份再写入保证可回滚。为什么建议这么做因为登录界面这东西看着简单但牵扯 getty、PAM、sshd 三条链路一旦你今天改了个 issue明天又去调了个 profile.d时间一长自己都记不清改过哪几个文件。维护脚本是给自己写的操作留痕你在里面每加一个函数就把对应的备份策略、配置文件、验证方法都固化下来了。我个人实际操作中的体会是登录界面从来没有一改就完美的说法一定是在使用中不断调整的。今天觉得字符画太占地方明天觉得 IP 放在最前面更直观有维护脚本在这些迭代成本会低很多。而脚本头的好处则是越早开始越受益哪怕你从今天开始只给新写的脚本加头部一个月后你的脚本资产就会形成一个井井有条的库。两件事都不难难的是养成习惯希望这篇文章能帮你迈出这一步。