30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

『Software Design 2019年6月号』の「思わず実践したくなるシェル&シェルスクリプト」を読んだ

初心者向けに書かれているので、読みやすかった。

コマンドがファイルとして定義されていることすら知らないレベルだったので、勉強になった。
分かっているようで分かっていなかった「パスを通す」の意味も分かったし。分かってしまえば、なんで分からなかったんだろうという感じではあるが。

シェルスクリプトで何が出来るのか分かっていないと自動化の発想すら出てこないので、基礎が分かってよかった。

gihyo.jp

以下、各章のメモ。

第1章 楽しく学ぶシェルの基本

例外もあるが、コマンドというのは、基本的にはファイル。
whichコマンドを使うと、コマンドのファイルがどこにあるか調べることが出来る。

$ which date
/bin/date
$ which git
/usr/local/bin/git
$ which node
/Users/$USER/.nodebrew/current/bin/node

標準出力、標準入力、標準エラー出力の3つは「標準ストリーム」と呼ばれる。

パイプ|は、左側のコマンドの標準出力を、右側のコマンドの標準入力にする。

リダイレクト>は、左側に書いたコマンドの標準出力先を、右側に書いたファイルにする。
>&2は、標準出力ではなく標準エラー出力を使うようにする命令。

;でコマンドをつなげることで、複数のコマンドをまとめて実行できる。これを「リスト」という。

$ echo a; echo b
a
b

「リスト」をまとめてパイプに渡すには、括弧で囲む。この記法を「サブシェル」という。

$ echo a; echo b | echo-sd -s
a
_人人_
> b <
 ̄Y^Y^ ̄
$ (echo a; echo b) | echo-sd -s
_人人_
> a <
> b <
 ̄Y^Y^ ̄

ちなみにecho-sdコマンドは以下の手順で利用できるようになる。

$ curl -OL https://git.io/echo-sd
$ sudo install -m 0755 echo-sd /usr/local/bin/echo-sd

第2章 テキスト処理で役立つシェル・テクニック

筆者の実際の原稿を題材に、テキスト処理を行う。
原稿は以下のリポジトリで公開されている。

github.com

grepコマンドで検索できる。wcコマンドは、入力された文字列の情報を返す。
以下は、カレントディレクトリ以下にある全ての.rstファイルを対象にできるを検索し、該当した行数、単語数、バイト数、を表示している。

$ grep できる *.rst | wc
87     150    8572

wc-lオプションを渡すと、行数のみが表示される。

$ grep できる *.rst | wc -l
87

awkFNRは、各ファイルの行数を持っている。

$ awk 'FNR==2' *.rst
開眼 シェルスクリプト 第1回
開眼シェルスクリプト 第2回
開眼シェルスクリプト 第3回
開眼シェルスクリプト 第4回
...

第3章 基本から押さえるシェルスクリプト

シェルスクリプトとは、シェルで実行するコマンドを順番に記述したテキストファイルのこと。
シェルスクリプトを用意することで、複雑なコマンドを簡単に実行できるようになる。

$ bash hoge.bashのようにシェル(この例ではbash)の引数にシェルスクリプトを渡すと、そのシェルスクリプトが実行される。

$ chmod +x hoge.bashでシェルスクリプトに実行権限を付与できる。これで、$ ./hoge.bashのようにシェルスクリプトを実行できるようになる。

./のようなディレクトリの指定をせずにシェルスクリプトを実行するには、そのシェルスクリプトをコマンド検索パスに含める必要がある。
コマンド検索パスはPATHという環境変数に入っているので$ echo $PATHで見れる。
ディレクトリを指定せずにシェルスクリプトを実行すると、このコマンド検索パスの中から探す。そのため、hoge.bashがここに含まれていなければ、$ hoge.bashとしたときにcommand not foundになってしまう。

$HOME/bin/PATHに含めたい場合は、$ PATH="${PATH}:${HOME}/bin/とする。
この状態でhoge.bash$HOME/bin/に入れておくと、$ hoge.bashを実行できる。

だがコマンド検索パスはログインする度に設定しないといけないので、.bash_profileに書くなどしてログイン時に自動的に反映されるようにしておくとよい。

位置変数とは、シェルスクリプトの実行時に渡した引数が格納されている変数。
$1$2のように、$と引数の順番の組み合わせになっている。
hoge.bashの内容が以下のときに$ hoge.bash abc xyzとすると、abcxyzが表示される。

echo $1
echo $2

コマンドは、実行結果を示す「終了ステータス」を返す。
コマンドが成功した場合は0を、エラーが発生した場合は0以外を返す。

シェルスクリプトのなかでexitコマンドを使うと、その引数を終了ステータスとして返し、コマンドが終了する。
以下のように書くと、abを表示したあと、終了ステータスとして0を返す。
exitでコマンドを終了するためcは表示されない。

echo a
echo b
exit 0
echo c

直前に実行したコマンドの終了ステータスは$?という変数に入っている。

$ foo.bash
-bash: foo.bash: コマンドが見つかりません
$ echo $?
127

ifforなどの制御構文も扱える。条件式にはtestコマンドを使うことが多い。

unix時間は$ date "+%s"で取得できる。

この特集では特に触れられていなかった(サンプルには記述されていた)が、シェルスクリプトの1行目には#!/bin/bashなどのシバンを書いたほうがいいはず。

#!/bin/sh は ただのコメントじゃないよ! Shebangだよ! - Qiita

第4章 効率的で安全なファイル操作の秘訣

set -eは、エラーが発生したらすぐにシェルスクリプトを終了させる。
set -uは、未定義の変数を使った場合はエラーにする。
この2つを組み合わせたset -euをシェルスクリプトの冒頭に書いておくことで、記述ミスした場合はすぐにエラーになり、シェルスクリプトが意図せぬ操作を行ってしまうことを防げる可能性が高まる。

以下のコマンドで、パス直下のディレクトリやファイルをファイルサイズの順に並べて表示できる。

$ du -s パス | sort -nr | awk '{print $NF}' | xargs du -sh

ファイル操作は安全が第一なので、削除や上書きは自動化しない。

Appendix 1 シェルの「展開」をマスターしよう

bashには「展開」という機能がある。*も展開の一種で、「パス名展開」という。

ブレース展開

{}でくくられた文字を展開する。区切り文字として,..を用いる。

$ echo a{x,y}b
axb ayb
$ echo a{1..3}b
a1b a2b a3b

x..y..zとすると、xから始まりyに到達するまで、xずつインクリメントしていく。

$ echo a{1..8..3}b
a1b a4b a7b

チルダ展開

~の後ろにつけた文字列をユーザー名と解釈し、そのユーザーのホームディレクトリパスを展開する。
~だけの場合は、実行したユーザーのホームディレクトリパスを展開する。

$ cd ~Guest
$ pwd
/Users/Guest

パラメータと変数展開

先頭に$をつけると、変数を展開できる。

$ hoge=HOGE
$ echo $hoge
HOGE

コマンド置換

$(コマンド)とすると、コマンドの実行結果を展開する。

$ echo 今日の日付は $(date '+%Y%m%d') です
今日の日付は 20190715 です

算術式展開

$((計算式))とすると、計算式の結果を展開する。

$ echo 10 / 2 の結果は $((10/2)) です
10 / 2 の結果は 5 です

単語の分割

展開された文字列を、改行や空白を区切り文字にして分割する。

パス名展開

*?[]をパターンとして認識しマッチしたファイルを展開する。

  • *
    • 任意の文字列にマッチ
  • ?
    • 任意の1文字にマッチ
  • []
    • 間に挟まれた任意の1文字にマッチ
$ ls *.txt
work_1.txt  work_10.txt work_2.txt  work_3.txt  work_4.txt  work_5.txt
$ ls work_?.txt
work_1.txt  work_2.txt  work_3.txt  work_4.txt  work_5.txt
$ ls work_[13].txt
work_1.txt  work_3.txt

プロセス置換

コマンドの出力結果を、別のコマンドの引数として扱える。
<(コマンド)を入力として、>(コマンド)を出力として、扱う。

$ diff <(echo -e 'a\nb\nc') <(echo -e 'a\nb\nz') -u
--- /dev/fd/63  2019-07-15 20:47:14.000000000 +0900
+++ /dev/fd/62  2019-07-15 20:47:14.000000000 +0900
@@ -1,3 +1,3 @@
 a
 b
-c
+z

Appendix 2 シェルのカスタマイズ方法

プロンプトは環境変数PS1で設定されている。

$ echo $PS1
\h:\W \u\$

noclobber

noclobberオプションを有効にすると、>によるファイルの上書きを禁止できる。
有効になっているかは、$ set -o | grep noclobberで確認できる。
$ set -Cで有効にできる。

$ set -o | grep noclobber
noclobber       off
$ set -C
$ set -o | grep noclobber
noclobber       on
$ echo hoge > foo.txt
$ echo fuga > foo.txt
-bash: foo.txt: 存在するファイルを上書きできません

nounset

未定義の変数を参照すると、空文字が返ってくる。

$ echo a${FOO}b
ab

nounsetオプションを有効にすると、未定義の変数を参照した際にエラーが発生するようになる。
$ set -uで有効になる。

$ set -o | grep nounset
nounset         off
$ set -u
$ set -o | grep nounset
nounset         on
$ echo a${FOO}b
-bash: FOO: 未割り当ての変数です

standard-version と commitlint で npm パッケージのリリース管理を省力化する

standard-versionというライブラリを使うことで、リリース管理に伴う作業のいくつかを自動化できる。
具体的には以下の内容。

  • package.jsonversionフィールドの値の更新
  • CHANGELOG.mdの更新
  • 更新したpackage.jsonCHANGELOG.mdのコミット
  • Git のタグを打つ

これらを手作業で行うのは不毛だなと以前から思っていたので、standard-versionを導入した。

この記事ではstandard-versionの基本的な使い方を紹介する。
また、このライブラリを使うためにはコミットメッセージが重要になるので、コミットメッセージをチェックするためのcommitlintについても紹介する。

この記事を書くにあたり、以下のバージョンで動作確認をした。

  • standard-version@6.0.1
  • @commitlint/config-conventional@8.0.0
  • @commitlint/cli@8.0.0

standard-version を導入する

適当なプロジェクトを作って、standard-versionが何をしてくれるのかを見ていく。

空行だけのindex.txtと、以下の内容のpackage.jsonを用意する。

{
  "name": "sample",
  "version": "1.0.0",
  "scripts": {
    "release": "standard-version"
  },
  "devDependencies": {
    "standard-version": "^6.0.1"
  }
}

$ yarnしてstandard-versionをインストールした状態で、以下のようにコミットする。

$ git commit -m "feat: initial commit"

そして、$ yarn run release --first-releaseを実行する。

すると、以下の内容のCHANGELOG.mdが生成され、コミットされる。

# Changelog

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## 1.0.0 (2019-07-10)


### Features

* initial commit 03cbea8

さらに、直近のコミットに対してv1.0.0というタグが打たれている。
なので現時点でのコミット履歴は以下の通り。

8d5cd91 (HEAD -> master, tag: v1.0.0) chore(release): 1.0.0
03cbea8 feat: initial commit

さらにコミットを重ねるためにindex.txtを更新する。

diff --git a/index.txt b/index.txt
index e69de29..e2e3369 100644
--- a/index.txt
+++ b/index.txt
@@ -0,0 +1 @@
+patch のテスト。

コミットとreleaseを行う。

$ git commit -m "fix: update index.txt"
$ yarn run release

すると、CHANGELOG.mdpackage.jsonが更新され、コミットされる。

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8b6241..19a4565 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
 
 All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-versio
n) for commit guidelines.
 
+### [1.0.1](///compare/v1.0.0...v1.0.1) (2019-07-10)
+
+
+### Bug Fixes
+
+* update index.txt f981c9c
+
+
+
 ## 1.0.0 (2019-07-10)
 
 
diff --git a/package.json b/package.json
index b2941ae..31a47e3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "sample",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "scripts": {
     "release": "standard-version"
   },

タグも打たれ、コミット履歴は以下のようになる。

$ git log --oneline
15db18f (HEAD -> master, tag: v1.0.1) chore(release): 1.0.1
f981c9c fix: update index.txt
8d5cd91 (tag: v1.0.0) chore(release): 1.0.0
03cbea8 feat: initial commit

standard-version がやっていること

standard-versionを実行すると、以下の4つを行う。

  1. package.jsonのバージョンを更新する
  2. CHANGELOG.mdを更新する
  3. package.jsonCHANGELOG.mdをコミットする
  4. タグをつける

だが、--first-releaseオプションを付けると1のバージョンアップは行わず、現行のpackage.json#versionの値のまま2以降を行う。
そのため、CHANGELOG.mdを最初から作るときにのみ--first-releaseオプションを付け、二回目以降、あるいは既にCHANGELOG.mdが存在する場合は、このオプションは付けない。

バージョンアップと CHANGELOG.md 生成のルール

コミットメッセージに接頭辞をつけることで、その接頭辞に応じてバージョンアップが行われる。
例えばfeatはマイナーアップデート、fixはパッチアップデートになる。
また、feat!のように接頭辞の後ろにエクスクラメーションマークをつけると、それは破壊的変更を含むことを意味し、メジャーアップデートになる。
choredocsでコミットした内容はCHANGELOG.mdには反映されない。

そのため、上述のサンプルの続きとして次のようなコミットをして$ yarn run releaseすると、バージョンは2.0.0になる。
CHANGELOG.mdにはfe90828についてのみ記載され、choreである1f1e208については何も書かれない。

fe90828 feat!: new feature
1f1e208 chore: library install
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19a4565..48f573b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,20 @@
 
 All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
 
+## [2.0.0](///compare/v1.0.1...v2.0.0) (2019-07-10)
+
+
+### Features
+
+* new feature fe90828
+
+
+### BREAKING CHANGES
+
+* new feature
+
+
+
 ### [1.0.1](///compare/v1.0.0...v1.0.1) (2019-07-10)

コミットのルールは Conventional Commits に準拠している。

www.conventionalcommits.org

Git のリモートリポジトリが登録されていれば、CHANGELOG.mdに記述されるコミット番号(上記のfe90828など)に、リモートリポジトリ上の当該コミットにリンクが張られる。

公式ドキュメントでは、プルリクエストのマージはSquash and Mergeで行うことを推奨している。
そうすると、CHANGELOG.mdの各項目に、当該プルリクエストへのリンクが自動的に生成される。

各種オプション

--dry-runオプション

--dry-runオプションをつけると、実際には何の変更も行うことなく、releaseしたときに何が行われるのかを確認することが出来る。

-tオプション

デフォルトでは、生成されるタグはバージョンにvという接頭辞がついたものになる。
この接頭辞は、-tオプションの引数で指定することが出来る。空文字を指定すれば(-t '')、接頭辞はつかずにバージョン番号のみのタグになる。

--release-asオプション

接頭辞に応じてバージョンアップしていくことは前述したが、--release-asオプションを使えば、次のバージョンを自分で指定することが出来る。

例として$ yarn run release --release-as 4.2.2 -t fooを実行すると、次のバージョンは4.2.2になり、foo4.2.2というタグが打たれる。

commitlint

standard-versionでのリリース管理は、コミットメッセージが重要になる。
コミットメッセージが正しくなければ上手く運用することは出来ない。

コミットメッセージが Conventional Commits に準拠しているか確認できるツールとして、commitlintがある。
commitlintはその名の通りコミットメッセージを対象とした Lint であり、コミットメッセージがルールに沿っているかチェックすることが出来る。この記事ではデフォルトのまま使うが、ルールの編集も当然行える。

ライブラリのインストールと、設定ファイルの出力を行う。

$ yarn add -D @commitlint/{config-conventional,cli}
$ echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

npm scripts"commitlint": "commitlint"を追加する。

$ yarn run commitlint --from=コミットIDを実行すると、指定したコミットののコミットから直近のコミットまでを、チェックする。

例えば、以下のようなコミットログになっているとする。

1d35a35 (HEAD -> master) feat: good commit 2
03b72d7 fix: good commit
b3cf4bc bad commit
a7eba0d feat: commitlint を導入した。

この状態で$ yarn run commitlint --from=b3cf4bcとすると、何もエラーは出ない。
$ yarn run commitlint --from=a7eba0dとすると、b3cf4bcもチェックの対象になり、エラーが出る。

⧗   input: bad commit
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings

コミットIDの代わりにタグを指定することも出来るし、--from=masterのようにブランチを指定することも可能。