【动机】
linux shell 脚本常见的调试手法如下:
- 将命令直接粘贴到控制台
- echo / printf 数值
- 断点调试, 例如 vscode 配合使用 Bash Debug 等调试插件
而日志记录常见用于生产环境下, 记录程序执行正常或者出错的情况, 同时, 当上述三种调试手段不适用, 或者实现难度很大时, 日志记录就可以在调试时发挥一定作用.
【意图】
将不同性质的日志内容分类存放同一目录下的不同的文件中, 如果是出错分成几个级别, 例如 warn error critical 等, 正常日志分 info success 等, 要求运行时不一定创建日志(依赖于目录是否存在), 调试时总是创建日志
【实现】
在 log.sh 中, 写入 log 函数如下
:<<COMMENT
记录日志, 用指定的日志级别名做文件名保存在 log/<level>.log
$1: content, default to "content forgotten."
$2: level, default to info
用法: log <content> <log_level>
注意:
1. 以下两个条件均满足, 则日志记录被忽略
1) 当前非调试态;
2) 日志目录不存在
2. ROOT_DIR_PATH 是入口脚本所在目录(绝对路径), 一般在入口脚本中计算, 和当前目录 . 不一定相同.
日志目录之父母录, 优先选择前者.
COMMENT
log(){
local dir=./log
[ "$ROOT_DIR_PATH" ] && dir=$ROOT_DIR_PATH/log
[ "$__DEBUG__" -a ! -d "$dir" ] && mkdir -p $dir
if [ -d "$dir" ]; then
local content=${1:-"content forgotten."}
local time=$(date +'%Y-%m-%d %H:%M:%S')
local level=$(available_range "$2" info $(echo ${__AVAI_OP__[@]}))
echo -e "[$time] [process id:$$] [${FUNCNAME[2]}] $content" >> $dir/${level}.log
fi
}
其中, ROOT_DIR_PATH 一般在入口脚本定义, 如果没有则以当前目录为准; __DEBUG__ 可以在任何文件中定义, 但一般为了同一管理, 也在入口文件中, 不存在或值为空, 则表示当前为非调试态.函数根据这两个全局只读变量, 决定当前是否记录日志, 以及日志文件存放于何处. 另外, __AVAI_OP__ 的定义, 位于 console.sh 中.
log 函数使用并不是很方便, 以下是指定日志 level 的函数, 这是采用纯静态的方法定义:
:<<COMMENT
记录一般信息日志到 /log/info.log 文件
$1: content
用法: log_info <content>
COMMENT
log_info(){
log "$*" info
}
log_error(){
log "$*" error
}
log_warn(){
log "$*" warn
}
log_success(){
log "$*" success
}
log_critical(){
log "$*" critical
}
当然, 还可以定义 log_tip / log_menu / log_caption 等等, 这依赖于 __AVAI_OP__ 的定义.
【测试】
新建 test.sh 文件, 输入以下代码:
# 根据实际文件位置修改路径
# source ./data.sh
# source ./log.sh
way1(){
local parts="info error warn success critical" fun
for part in $parts; do
fun=log_$part
$fun "this log content is by $fun function"
done
}
way1
unset -f way1
当 __DEBUG__ 为空, 日志记录操作被忽略; 否则, 将在当前目录(或者入口脚本根目录)下, 产生以下的 log 目录结构:

每个文件新添内容大致如下:
[2025-03-16 15:30:02] [process id:17807] [way1] this log content is by log_info function
【必须三思的扩展】
那么是否可以取消 log 函数中的 level 级别的可用性检测, 继而实现动态生成 log_xxx 形式的函数呢? 答案是可以的.
新建 logx 函数, 与 log 函数基本相同, 但是去除了 level 级别的可用性检测, 仍然放在 log.sh 文件中
# $1: content, default to "content forgotten."
# $2: level, default to info
logx(){
local dir=./log
[ "$ROOT_DIR_PATH" ] && dir=$ROOT_DIR_PATH/log
[ "$__DEBUG__" -a ! -d "$dir" ] && mkdir -p $dir
if [ -d "$dir" ]; then
local content=${1:-"content forgotten."}
local time=$(date +'%Y-%m-%d %H:%M:%S')
echo -e "[$time] [process id:$$] [${FUNCNAME[2]}] $content" >> $dir/$2.log
fi
}
要动态生成日志记录函数, 需要重写系统内置函数 command_not_found_handle. 考虑到该函数的系统级别, 故将它放在 system.sh , 而不是前面的 log.sh, 这样也方便将来扩展(如果需要一些其他类型的动态函数).
:<<COMMENT
当调用 logx_{XXX} 形式的函数时, 自动剥离出 level, 然后调用 logx 函数,
而其他函数, 则执行系统默认处理
COMMENT
command_not_found_handle() {
if [[ "$1" =~ ^logx_ ]]; then
local level=${1#*logx_}
shift
logx "$@" $level
else
bash -c "command $1" # 或者其他方式调用默认处理器
return 127
fi
}
【测试】
新建 test.sh 文件, 写入以下代码:
# 根据实际文件位置修改路径
# source ./system.sh
# source ./log.sh
way2(){
logx_A "custom leve A log"
logx_B "B log content"
}
way2
unset -f way2
运行结果显示, log 目录下生成文件 A.log, B.log, 并写入了相应内容. 图略
【看上去很优雅】
但是, 必须注意, 这样扩展的日志记录函数在代码简洁的同时, 也为调用端提供了生成无穷 level 日志的可能, 其结果就是日志文件过多, 反过来会不容易管理. 所以, 实践中, 这样的动态函数还是慎用!
谢谢观看.