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: 未割り当ての変数です