名称: shfmt-formatting 用户可调用: false 描述: 在使用shfmt格式化shell脚本时使用。涵盖一致的格式化模式、shell方言支持、常见问题和编辑器集成。 允许的工具:
- 读取
- 写入
- 编辑
- Bash
- Grep
- Glob
shfmt 格式化
关于shfmt的格式化能力、模式以及集成到开发工作流中以实现一致shell脚本格式化的专家知识。
概述
shfmt格式化shell脚本以提高可读性和一致性。它将脚本解析成AST并以规范格式打印,消除风格争议并确保跨代码库的统一性。
支持的Shell方言
shfmt支持多种具有不同语法规则的shell方言:
POSIX Shell
最便携,适用于任何Unix系统上的/bin/sh:
#!/bin/sh
# 仅使用POSIX兼容语法
# 无数组
# 无[[ ]]测试
# $()必须在所有上下文中工作
if [ "$var" = "value" ]; then
echo "match"
fi
Bash
脚本中最常见,支持扩展功能:
#!/usr/bin/env bash
# 允许Bash特定功能
declare -a array=("one" "two" "three")
if [[ "$var" == "value" ]]; then
echo "match"
fi
result=$((1 + 2))
mksh(MirBSD Korn Shell)
Korn shell变体,具有自己的扩展:
#!/bin/mksh
# mksh特定语法
typeset -A assoc
assoc[key]=value
Bats(Bash Automated Testing System)
用于Bats测试文件:
#!/usr/bin/env bats
@test "示例测试" {
run my_command
[ "$status" -eq 0 ]
}
格式化模式
缩进
shfmt在整个脚本中规范化缩进:
之前:
if [ "$x" = "y" ]; then
echo "两个空格"
echo "四个空格"
echo "制表符"
fi
之后(使用-i 2):
if [ "$x" = "y" ]; then
echo "两个空格"
echo "四个空格"
echo "制表符"
fi
间距
shfmt规范化操作符和关键字周围的间距:
之前:
if[$x="y"];then
echo "无间距"
fi
x=1;y=2;z=3
之后:
if [ $x = "y" ]; then
echo "无间距"
fi
x=1
y=2
z=3
分号和换行符
多个语句被拆分成单独的行:
之前:
if [ "$x" ]; then echo "是"; else echo "否"; fi
之后:
if [ "$x" ]; then
echo "是"
else
echo "否"
fi
Here Documents
Here-docs被保留,但缩进被规范化:
之前:
cat <<EOF
行 1
行 2
EOF
之后(保留):
cat <<EOF
行 1
行 2
EOF
使用<<-的缩进here-docs允许制表符剥离:
if true; then
cat <<-EOF
缩进内容
更多内容
EOF
fi
函数定义
函数被一致格式化:
之前:
function my_func
{
echo "旧风格"
}
my_func2 () { echo "单行"; }
之后:
function my_func {
echo "旧风格"
}
my_func2() {
echo "单行"
}
使用-fn(函数下一行):
function my_func
{
echo "大括号在新行"
}
Case语句
Case语句被一致格式化:
之前:
case "$1" in
start) do_start;;
stop)
do_stop
;;
*) echo "未知";;
esac
之后:
case "$1" in
start)
do_start
;;
stop)
do_stop
;;
*)
echo "未知"
;;
esac
使用-ci(case缩进):
case "$1" in
start)
do_start
;;
stop)
do_stop
;;
esac
二元操作符
二元操作符的行延续:
默认:
if [ "$a" = "foo" ] &&
[ "$b" = "bar" ]; then
echo "匹配"
fi
使用-bn(二元下一行):
if [ "$a" = "foo" ] \
&& [ "$b" = "bar" ]; then
echo "匹配"
fi
重定向
重定向格式化:
默认:
echo "hello" >file.txt
cat <input.txt 2>&1
使用-sr(空格重定向):
echo "hello" > file.txt
cat < input.txt 2>&1
常见格式化问题
问题:混合制表符和空格
问题: 脚本有缩进不一致
解决方案:
# 全部转换为空格(2空格缩进)
shfmt -i 2 -w script.sh
# 或全部转换为制表符
shfmt -i 0 -w script.sh
问题:尾部多余分号
问题: 行尾有不必要的分号
之前:
echo "hello";
x=1;
之后(shfmt移除它们):
echo "hello"
x=1
问题:不一致的引号
shfmt保留引号风格,但规范化不必要的引号:
之前:
echo 'single' "double" $'ansi'
x="simple"
之后(保留):
echo 'single' "double" $'ansi'
x="simple"
问题:长行
shfmt不会自动换行。使用手动行延续:
# 长命令延续
very_long_command \
--option1 value1 \
--option2 value2 \
--option3 value3
问题:数组格式化
数组根据原样格式化在单行或多行:
# 单行(保留)
array=(one two three)
# 多行(保留)
array=(
one
two
three
)
编辑器集成
VS Code
安装“shell-format”扩展:
// settings.json
{
"shellformat.path": "/usr/local/bin/shfmt",
"shellformat.flag": "-i 2 -ci -bn",
"[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format",
"editor.formatOnSave": true
}
}
Vim/Neovim
使用ALE:
" .vimrc
let g:ale_fixers = {
\ 'sh': ['shfmt'],
\}
let g:ale_sh_shfmt_options = '-i 2 -ci -bn'
let g:ale_fix_on_save = 1
使用原生格式化:
" .vimrc
autocmd FileType sh setlocal formatprg=shfmt\ -i\ 2\ -ci
Emacs
使用reformatter:
;; init.el
(use-package reformatter
:config
(reformatter-define shfmt
:program "shfmt"
:args '("-i" "2" "-ci")))
(add-hook 'sh-mode-hook 'shfmt-on-save-mode)
JetBrains IDE
安装“Shell Script”插件,在设置中配置: 设置 -> 工具 -> Shell Scripts -> Formatter
Path to shfmt: /usr/local/bin/shfmt
Options: -i 2 -ci -bn
Diff和检查模式
检查格式化(CI模式)
# 显示将更改的差异(如果需要更改则退出1)
shfmt -d script.sh
shfmt -d .
# 列出需要格式化的文件
shfmt -l .
# 退出代码:
# 0 = 无需更改
# 1 = 需要更改或错误
原地格式化
# 用格式化版本覆盖文件
shfmt -w script.sh
shfmt -w .
预览更改
# 输出格式化版本到标准输出
shfmt script.sh
# 输出格式化版本到文件
shfmt script.sh > formatted.sh
与Git配合使用
格式化暂存文件
# 仅格式化暂存的shell脚本
git diff --cached --name-only --diff-filter=ACM | \
grep '\.sh$' | \
xargs -r shfmt -w
预提交钩子
#!/usr/bin/env bash
# .git/hooks/pre-commit
# 检查shfmt是否可用
if ! command -v shfmt &>/dev/null; then
echo "shfmt not found, skipping format check"
exit 0
fi
# 获取暂存的shell文件
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sh$')
if [ -n "$files" ]; then
# 检查格式化
if ! echo "$files" | xargs shfmt -d; then
echo "Shell scripts need formatting. Run: shfmt -w <files>"
exit 1
fi
fi
格式化更改的文件
# 格式化自main分支以来更改的文件
git diff --name-only main...HEAD | \
grep '\.sh$' | \
xargs -r shfmt -w
压缩(高级)
shfmt可以通过移除空格压缩脚本:
# 压缩脚本
shfmt -mn script.sh > script.min.sh
之前:
#!/bin/bash
# 注释
function hello {
echo "Hello, World!"
}
hello
之后(压缩后):
#!/bin/bash
function hello { echo "Hello, World!"; }
hello
注意:压缩移除注释和大多数空格。仅用于分发,不用于开发。
最佳实践
- 保存时格式化 - 配置编辑器自动格式化
- CI验证 - 在CI流水线中运行
shfmt -d - 一致的团队设置 - 提交
.shfmt.toml到仓库 - 匹配Shebang - 确保shell方言匹配脚本shebang
- 格式化后审查 - 验证更改合理
- 不混合风格 - 在所有脚本中使用相同设置
- 预提交钩子 - 防止未格式化代码被提交
故障排除
解析错误
如果shfmt解析脚本失败:
# 先检查语法
bash -n script.sh
# 或对于POSIX
sh -n script.sh
错误方言检测
强制正确的方言:
shfmt -ln bash script.sh
shfmt -ln posix script.sh
保留有意格式化
对于不应重新格式化的代码,考虑:
- 将其移动到单独的文件,不由shfmt处理
- 使用
# shfmt:ignore注释(不支持 - 使用文件排除) - 接受格式化版本
何时使用此技能
- 为一致性格式化shell脚本
- 将shfmt集成到开发工作流
- 解决脚本中的格式化问题
- 在编辑器中设置保存时格式化
- 配置CI/CD格式检查
- 理解shfmt的格式化决策