hkoba blog

プログラマーです。プログラミング言語ミーハーです。ツッコミ歓迎です。よろしくどうぞ(能代口調)

Dry-run の実現技法、個人的な定番

X こと元Twitterid:Windymelt さんの記事が目に入ったので、自分が最近使っている Dry-run の実現技法、イディオムについてまとめておこうと思います。

blog.3qe.us

自分は Dry-run はオプションで指定する派です

理由は以下の通りです。

  • 環境変数は子プロセスに無限に伝わるので、本質的にアンコントローラブル。迷結合によるトラブルの源となりかねない。
  • 自分的にコマンド行オプションで Dry-run 指示を伝えるイディオムが確立していること。

Zsh で Dry-run、自分のやり方

Zsh での dry-run の実現方法については以前の記事でも書きました。 その後も進化があって、現時点での基本形はこうです。

emulate -L zsh;  # 大前提

# 脳に収まる、手短なバージョン
zparseopts -D -K n=o_dryrun

function x {
    print "# $@"
    if (($#o_dryrun)); then return; fi
    "$@" || exit $?
}

使い方は、dry-run にしたいコマンド行の先頭にこの関数 x を加えるだけです。(このぐらいお手軽に足せないと、Dry-run を実装しようという気にならない)

x curl -fsSL --compressed --output $copiedBin/cpm https://git.io/cpm

x sudo chmod +x $copiedBin/cpm

# dry-run オプションを別のコマンドに渡したいときも、変数を書くだけ
make $o_dryrun

実際の使用例はこちら

この基本形を元に、状況に応じて改変したものを用いています。

  • -q (quiet)とか -s (silent) 等のオプションが渡されたときは Dry-run 出力を抑制
  • Dry-run の出力先を stderr に
  • Dry-run 出力をコピペで実行できる形式にシリアライズさせる print -R "#" "${(q-)@}"
  • 対話 shell 内で source して使いたいスクリプトなら exit $?return $? に変更

この技法の限界

残念ながら、この技法ではパイプやリダイレクトを使ったコマンド行の Dry-run 化は出来ません。パイプやリダイレクトは Zsh 自身の構文なので、 x の引数には出来ないからです。

x find -name \*.bak | xargs rm

x ls > /tmp/files.out

# 動くけど、Dry-run の出力に stdin の 内容が画面に出ないのが残念
x at 02:00 <<<"dnf update -y httpd; systemctl restart httpd"

この弱点があるため、最近の私は雑用スクリプトも Tcl/Tk で書くように切り替えつつあります。

Tcl/Tk で Dry-run、自分のやり方

基本的な考え方は Zsh 版と同じで、実行したいコマンド行全体を引数として取る手続きを用います。

Tcl の exec コマンドはデフォルトではコマンドの stdout を戻り値に、エラー出力を例外に変換するので、シェルスクリプトっぽい使い勝手を実現するためにリダイレクト指定を加えています。

package require cmdline

array set ::opts [cmdline::getoptions ::argv {
  {n "dry-run"}
}]

proc RUN args {
    puts "# $args"
    if {$::opts(n)} return
    # リダイレクトが指定されていない時は stdout へリダイレクト。末尾のみ認識。
    if {[lindex $args end-1] ni {">" ">@" ">>"}} {
        lappend args >@ stdout
    }
    =RUN {*}$args
}

proc =RUN args {
    exec -ignorestderr {*}$args 2>@ stderr
}

proc o_dryrun {} {
    if {$::opts(n)} {list -n}
}

# Tcl 自体のコマンドを dry-run にしたいときは ** を使う
proc ** args {
    puts "# $args"
    if {$::opts(n)} return
    {*}$args
}

使い方

# 単純なコマンド実行なら、
# Tcl でも bash 等ボーン系のシェルのコマンド行が(ある程度)コピペで実行できます。

RUN curl -fsSL --compressed --output $copiedBin/cpm https://git.io/cpm

RUN sudo chmod +x $copiedBin/cpm

# 別のコマンドを実行しつつ dry-run オプションを渡したいとき
=RUN make {*}[o_dryrun]  >@ stdout

# パイプも ok
RUN find -name \*.bak | xargs rm

# リダイレクトも ok
RUN ls > /tmp/files.out

# stdin に与える内容も Dry-run 出力に出すことが出来る
RUN at 02:00 << "dnf update -y httpd; systemctl restart httpd"


# Tcl のコマンドを Dry-run にしたいときも同じ考え方
** file delete {*}[glob -nocomplain *.bak]

Tcl は (引数も戻り値も)全ては文字列である という思想で設計されています。

Tcl にとっては、パイプやリダイレクトの記号も構文ではなく単なる文字列引数であり、それを解釈するのは exec コマンドの役割です。昔は Tcl の構文のこの低機能さが嫌いでしたが、そのお陰で Dry-run が実装しやすくなっていることに最近気づいて、考えが変わりました。また、Tcl で実行されるコマンド自体が Tcl のリストとして表現されていることも、Dry-run の実装しやすさに貢献しているように思います。

(それはそれとして… Tcl の Dry-run の実装のしやすさを保ったままで、TypeScript 的な静的検査も行える言語が作れないものか…とは思ったり…)

ここまで読んで下さりありがとうございました。


  • 2025-02-09 使用例に標準入力のリダイレクトも追記