Webナイト宮崎 Vol.22

Shell(Bash)のツラミ(の一部)

Tcl(ティクル)で楽になる、かも?

by @hkoba

自己紹介

  • 今日は Shell と Tcl の話

皆さんへ質問(1/2)

雑用を Shell ←vs→ Python etc どっちで書くかの悩み、無いすか?

  • 例:社内ツールの導入手順
    git clone ... && cd ...
    chmod g+ws var var/db
    chown nginx var var/run
    sqlite3 var/db/myapp.db3 < sql/schema.sql
    
    • 外部コマンドの呼び出しやファイル操作の、単なる羅列

    • 他にも:CI/CD の中のアクションとか

    • 短いから Shell で…

皆さんへの質問(2/2)

けど、Shell(Bash)で苦しんだこと、無いすか?

  • 例:作業ディレクトリへ、データファイルをコピー
    workDir=_build
    
    fileList=*.dat
    
    cp --update $fileListtt $workDir;  # ←変数名間違い!なのにコマンド実行!
    
    ...                                # なのに停止しない!実行が続いてしまう!
    
    • 変数名の打ち間違い がエラーにならなず、実行されちゃう!
    • エラーが起きても停止しない!
    • 他にも:ファイル名にスペースが入る時に、書き方に注意が必要
  • そんな悩みに、Tcl

Tcl ってどんな言語?

  • Shell に似た構文
    # 変数代入
    set workDir _build
    
    #            ↓glob はワイルドカードにマッチするファイルのリストを返すコマンド
    set fileList [glob -nocomplain *.dat]
    
    # exec で外部コマンド呼び出し({*} はリスト展開)
    exec cp --update {*}$fileList $workDir
    
    • 文字列に "..." 不要、引数の区切りに , 不要(Shell に近い書き味
    • Shell 向けのコマンド例が流用できる(ある程度)
  • 変数名・コマンド名の間違いは即エラーで停止、例外処理へ

(個人的) Tcl の最推しポイント

  • コマンドとリストが同じデータ構造
    foo bar baz; # コマンド foo に引数 bar baz を渡す
    
    set cmd [list foo bar baz]; # 上記の処理を表す list を変数 cmd に入れる
    
    {*}$cmd; # 変数に入ったコマンドを実行
    
  • つまり:コマンドを操作するコマンドを作りやすい

  • Dry-Runべき等undo の仕組みを作りやすい

    • Dry-Run - 試運転(コマンドを表示するだけ。実行はしない。make -n

    • べき等(idempotent) - 目標状態を満たしてない時だけ実行(何回呼んでも安全)

比較

Dry-Run を題材に

Shell と Tcl を比較します

Dry-Run の実装例:Shellの場合

例: clean-backup.zsh (ここだけ Zsh です)

#!/bin/zsh
emulate -L zsh; setopt no_no_match
zparseopts -D -K n=o_dryrun; # -n なら配列 o_dryrun に保存
function x {; # `-n` オプションで dry-run にする shell 関数 `x`
    print "# $argv"
    if (($#o_dryrun)); then return; fi; # -n ならここでリターン
    "$argv[@]" || exit $?
}
#==== 以下、やりたいこと ====
x rm -f *.bak

使い方

./clean-backup.zsh -n
# rm -f ファイル… を表示するけど、実行はしない

Shell の Dry-Run の限界

先の仕組みでは dry-run 化できないもの:リダイレクトパイプ

# リダイレクト
x echo foobar > foo.txt

# パイプ
x find -size +10M | xargs rm -f

> |Shell の組み込み構文 → 関数の外側で動いてしまう

Tcl ではリダイレクト・パイプは exec が解釈する

Tcl ではリダイレクトやパイプは言語の構文ではない

exec echo foobar > foo.txt

exec find -size +10M | xargs rm -f

exec コマンドが 単なる引数 文字列として >| を受け取り、それを解釈して実行

Dry-Run の実装例:Tcl版

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

proc =RUN args {
    exec -ignorestderr {*}$args 2>@ stderr
}
proc RUN args {
    puts "# $args"
    if {$::opts(n)} return
    # 最後の手前の引数にリダイレクトが指定されていない時は stdout へリダイレクト
    if {[lindex $args end-1] ni {">" ">@" ">>"}} {
        lappend args >@ stdout
    }
    =RUN {*}$args
}

Tcl版の使用例

exec を RUN に変えるだけ: リダイレクトパイプ も OK

RUN echo foobar > foo.txt

RUN find -size +10M | xargs rm -f

変数間違いも、Dry-Run 時点でエラーになる

set fileList [glob *.bak]

RUN rm {*}$fileListtt;  # ERROR! ここで止まる

お断り:Tcl にも弱点・不満は沢山〜

  • 動的スコープなのに厳格 ← Shell慣れした人のつまずきポイント

  • 静的検査が無い(実行するまで typo すら見つからない)

  • 記述が長くなりがち

  • pip, gem, npm みたいなの無いの?(→ tcllibtcler's wiki で我慢)

  • tclsh に -e EXPR, -c CMD みたいなオプションが無いのはとても不便

  • 全てを任せる言語じゃない、けど、雑用言語としては有益

まとめ(伝えたいこと)

  • Tcl は コマンド行に対するメタプログラミング が出来る Shell風言語!!?

    • (大げさに言えば、Lisp をコマンド文字列のために再設計したかのような…)
  • もちろん Bash 全部の置き換えは無理

    • インストールが要るから
  • けど 仕方なく bash を使っている箇所何割かは、置き換えても良いかも?

  • ご清聴、ありがとうございました〜〜

関連記事

以前の blog: → Dry-run の実現技法、個人的な定番

Bash と Tcl の比較(1/2)

bash tcl
予約語 case do done elif else esac fi for function if in select then until while time (無し)
コメント # から行末まで コマンド名の位置の # から行末まで(ただし { } が優先される)
他の特殊文字 | & ; ( ) { } [ ] < > $ ` ! \ * ? = ; { } [ ] $ \

Bash と Tcl の比較(2/2)

bash tcl
外部コマンドの起動 予約語以外は全て外部コマンドと解釈される exec, open コマンドを用いて起動(原則)
パイプ
・リダイレクト
構文に組み込み | < > exec, open コマンドの機能
子プロセスの fork 構文に組み込み ( ) (外部パッケージ Tclx)

べき等の例

# Google Cloud の現在のプロジェクトの VM の一覧を取得
set vmList [=RUN gcloud compute instances list --format=value(name)]

if {$newVm in $vmList} {
  puts "VM $newVm は作成済み"
} else {
  RUN gcloud compute instances create $newVm ...
}

undo の例

# @ は順方向のときはそのまま RUN, 逆実行の時は変数 @ に貯めるようなコマンドにしておく
@ gcloud compute networks create $vpcName \
    --project=$project -- \
    --subnet-mode=custom--mtu=1460 ...

@ gcloud compute firewall-rules create $vpcName-allow-icmp-ssh \
    --project=$project \
    -- \
    "--description=google cloud 内部からのみ icmp, ssh を許可" \
    --network=projects/$project/global/networks/$vpcName ...
...
# -R オプションが指定されたときは、逆順に実行
if {$::opts(R)} {
    puts "====================="
    foreach cmd [lreverse [set ::@]] {
        RUN {*}$cmd
    }
}