CLOVER🍀

That was when it all began.

shfmtでシェルスクリプトのフォーマットを行う

これは、なにをしたくて書いたもの?

shfmtを使うと、シェルスクリプトのフォーマットができるようです。今回はUbuntu Linux 24.04 LTSに導入してみます。

shfmt

shfmtは、以下のGitHubリポジトリーに含まれるツールのひとつです。

GitHub - mvdan/sh: A shell parser, formatter, and interpreter with bash support; includes shfmt

文字通り、シェルスクリプトのフォーマットができます。

デフォルトのフォーマットスタイルはこちら。

https://github.com/mvdan/sh/blob/v3.8.0/syntax/canonical.sh

ドキュメントはこちら。

https://github.com/mvdan/sh/blob/v3.8.0/cmd/shfmt/shfmt.1.scd

環境

今回の環境はこちら。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.3 LTS
Release:        24.04
Codename:       noble


$ uname -srvmpio
Linux 6.8.0-79-generic #79-Ubuntu SMP PREEMPT_DYNAMIC Tue Aug 12 14:42:46 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

shfmtをインストールする

shfmtは、Linuxディストリビューションのパッケージマネージャーでインストールできます。

Ubuntu Linuxの場合はaptですね。

$ apt show shfmt
Package: shfmt
Version: 3.8.0-1
Built-Using: golang-1.21 (= 1.21.6-1), golang-github-google-renameio (= 2.0.0-2), golang-github-pkg-diff (= 0.0~git20210226.20ebb0f-1), golang-golang-x-sync (= 0.6.0-1), golang-golang-x-sys (= 0.16.0-1), golang-golang-x-term (= 0.16.0-1), golang-mvdan-editorconfig (= 0.2.0+git20231228.1925077-1)
Priority: optional
Section: universe/utils
Source: golang-mvdan-sh
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Debian Go Packaging Team <team+pkg-go@tracker.debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 3,038 kB
Enhances: bash, dash, mksh
Homepage: https://github.com/mvdan/sh
Download-Size: 1,131 kB
APT-Sources: http://jp.archive.ubuntu.com/ubuntu noble/universe amd64 Packages
Description: format shell programs
 shfmt is shell formatter, which supports POSIX Shell, Bash, and mksh.

インストールしてみます。

$ sudo apt install shfmt

Ubuntu Linux 24.0.4 LTSでは、3.8.0がインストールされました。

$ shfmt --version
3.8.0

ヘルプ。

$ shfmt --help
usage: shfmt [flags] [path ...]

shfmt formats shell programs. If the only argument is a dash ('-') or no
arguments are given, standard input will be used. If a given path is a
directory, all shell scripts found under that directory will be used.

  --version  show version and exit

  -l,  --list      list files whose formatting differs from shfmt's
  -w,  --write     write result to file instead of stdout
  -d,  --diff      error with a diff when the formatting differs
  -s,  --simplify  simplify the code
  -mn, --minify    minify the code to reduce its size (implies -s)
  --apply-ignore   always apply EditorConfig ignore rules

Parser options:

  -ln, --language-dialect str  bash/posix/mksh/bats, default "auto"
  -p,  --posix                 shorthand for -ln=posix
  --filename str               provide a name for the standard input file

Printer options:

  -i,  --indent uint       0 for tabs (default), >0 for number of spaces
  -bn, --binary-next-line  binary ops like && and | may start a line
  -ci, --case-indent       switch cases will be indented
  -sr, --space-redirects   redirect operators will be followed by a space
  -kp, --keep-padding      keep column alignment paddings
  -fn, --func-next-line    function opening braces are placed on a separate line

Utilities:

  -f, --find   recursively find all shell files and print the paths
  --to-json    print syntax tree to stdout as a typed JSON
  --from-json  read syntax tree from stdin as a typed JSON

For more information, see 'man shfmt' and https://github.com/mvdan/sh.

テスト用に、canonical.shを修正したこんなスクリプトを用意。

script.sh

#!/bin/bash
  ! foo bar >a &

foo() { bar; }

{
  var1="some long value" # var1 comment
    var2=short             # var2 comment
}

if foo; then bar; fi

for foo in a b c
do
        bar
done

case $foo
in
  a) A ;;
  b)
        B
        ;;
esac

foo | bar
foo &&
        $(bar) &&
        (more)

foo 2>&1
foo <<-EOF
        bar
EOF

`(3 + 4)`

実行は、スクリプトまたはディレクトリーを指定して行うようです。

$ shfmt script.sh

オプションをなにも指定しないと、結果が標準出力に書き出されます。

#!/bin/bash
! foo bar >a &

foo() { bar; }

{
        var1="some long value" # var1 comment
        var2=short             # var2 comment
}

if foo; then bar; fi

for foo in a b c; do
        bar
done

case $foo in
a) A ;;
b)
        B
        ;;
esac

foo | bar
foo &&
        $(bar) &&
        (more)

foo 2>&1
foo <<-EOF
        bar
EOF

$( (3 + 4))

なにが変わったのかわかりにくいので、-dオプションをつけると差分のみが表示されます。

$ shfmt -d script.sh
--- script.sh.orig
+++ script.sh
@@ -1,24 +1,22 @@
 #!/bin/bash
-  ! foo bar >a &
+! foo bar >a &

 foo() { bar; }

 {
-  var1="some long value" # var1 comment
-    var2=short             # var2 comment
+       var1="some long value" # var1 comment
+       var2=short             # var2 comment
 }

 if foo; then bar; fi

-for foo in a b c
-do
+for foo in a b c; do
        bar
 done

-case $foo
-in
-  a) A ;;
-  b)
+case $foo in
+a) A ;;
+b)
        B
        ;;
 esac
@@ -33,4 +31,4 @@
        bar
 EOF

-`(3 + 4)`
+$( (3 + 4))

また-wオプションをつけると直接ファイルを修正します。

いろいろ試してみましたが、個人的には以下のオプション指定で使いたいですね。

$ shfmt -i 4 -sr -bn -d -w [スクリプトまたはディレクトリー]

$ shfmt -l -i 4 -sr -bn -w [スクリプトまたはディレクトリー]

意味はこちら。

  • -lまたは--list … 変更するファイルパスを表示
  • -iまたは--indent … インデントを指定。デフォルトはタブで、数字を指定するとスペースの数の意味になる
  • -srまたは--space-redirects … リダイレクト演算子を使う時にスペースを入れる
  • -bnまたは--binary-next-line&&|のようなバイナリー演算子で行を開始する
  • -dまたは--diff …変更差分を表示する
  • -wまたは--write … 結果を標準出力に書き出すのではなくファイルを変更する

-l-dは相反するので、どちらかですね。

こんな感じで使っていこうと思います。