目次

この記事は筆者 syui による、UNIX & Linux コマンド・シェルスクリプトに関する Tips をまとめたものになります。

著者の環境は MacBook Air をメインに、OS には Arch Linux 及び MacOS を使うことが多いです。補助的に Alpine Linux と Windows も使います。ただ、各環境で完全にテストしているわけではないので、コマンド・シェルスクリプトの実行結果が当記事の内容と異なる場合があります。

また、現在メインで使用しているシェルはzshfishです。したがって、bashにもできる限り通用するよう心がけていますが、内容にシェル互換がないことがあるかもしれません。

最後に、著者はコンピュータについては全くの初心者です。多分、はじめてコンピュータを触ったばかりの人よりも遥かに劣るレベルだと思われます。その点は考慮していただけると幸いです。一緒にシェルスクリプトを学習していきましょう。

はじめに

シェルスクリプトについて

シェルスクリプトは解読に難はあるものの(ようは読みづらい)、何らかの仕組みを作る上では便利であり、簡単にできるものだと思っています。その理由の一つがシェルのシンプルな仕組みにあります。

シェル・コマンドの仕組みは、物事をシンプル簡単に処理することにあります。具体的には、一つのコマンドを叩けば結果が出力される仕組みです。分かりやすいですよね。

更に、それらをパイプを使って繋げることで、出力は変化していきます。これも特別難しいものではありません。

この出力を自在に操ることでシェルスクリプトは作成されます。

シェルスクリプトというのは、単純にコマンドの羅列に過ぎません。

このような仕組みは非常に分かりやすく、かつ簡単に実行可能です。

そして、やりたいことを実現できる能力も備わっています。

それは、世の中には便利なコマンドがたくさんあるからです。

これは最新技術にも言えることで、最新技術の多くはCLIコマンドを用意しています。

また、プログラムやシステムの初期段階では、簡易化されたコマンドやシェルスクリプトを使用する傾向が少なからずあります。

したがって、シェルスクリプトを学ぶ意味は現代においても十分にあると考えています。

この記事では、筆者がシェルスクリプトを使用するにあたって便利だと思った内容を中心にシェルスクリプトについて書いていきたいと思います。

方針について

当記事の方針の一つは、著者が後々利用しやすいように作っていく方針です。

一部、表現等に正確ではないものも含まれると思いますが、この記事では正確さよりも分かりやすさを重視します。もし正確な情報が欲しければ、公式や$ man bashなどを参照してください。

なお、内容が充実してきたら別ページにも保存するかもしれません。

シェルスクリプト入門

シェルスクリプトとは

シェルスクリプト(shell script)とは、基本的にはシェルを使って実行するコマンドの羅列です。これはテキストファイルに書き込みます。

まずは、難解なプログラムのイメージではなく、単に各種コマンドをつなぎ合わせたものだと考えてください。このようにイメージすることで、プログラミングに馴染みがない人にも心理的抵抗は少なくなるはずです。

コマンドの中には構文コマンドなども多数存在しており、わざわざテキストファイルから実行しなくても実行できます。以下はfor文を直接実行した例です。

$ for i in *;do echo ${i%.*} && pwd;done

しかし、このようなワンライナー(1行に詰め込まれたコマンドのこと)を何度も直接実行するのは面倒で、かつ解読にも難があることから、テキストファイルに記述して実行することがあります。

シェルスクリプトは主にこのような目的で作成される事が多いです。

以下は上記をシェルスクリプト形式で記述した例です。

#!/bin/zsh
for i in *
do
	echo ${i%.*} \
	&& pwd
done

こちらのほうが何をやっているのか分かりやすいですよね。

また、当該コマンドを後々何度も実行することが予想される場合、コマンドではなく、シェルスクリプトとして保存しておいたほうが便利になります。何度も難解なコマンドを手動で入力するのは煩雑だからです。

シェルとは

シェルスクリプトがコマンドの羅列だとして、シェルとは何でしょう。

ここで言うシェル(shell)とは、ユーザーが間接的にカーネルとの意思疎通を行うためのツールのことです。具体的にはbashzsh, fishがシェルに該当します。このように、シェルにも様々な種類があり、ユーザーが主に好みによって使用するシェルを選択します。

また、カーネル(kernel)とはOSの核で、主にハードとソフト(アプリ)の仲介を行います。

ターミナルなど

シェルを理解する上では、ターミナル(terminal)などの概念も重要になってきます。シェルを自在に操る人は、一般人から見ると真っ黒な画面で何やら怪しげなことをしているコンピューター・ハッカーにしか見えません。

ここで、真っ黒な画面がターミナルと呼ばれるアプリになります。そして、このターミナルアプリで実行されているのがシェルです。

シェルを通して処理されるものがコマンドということになります。

このようにコマンド(文字列)でコンピュータを操作する操作画面のことをCLI(コマンドラインインターフェイス)と言います。

ハッカー(hacker)が好んで使用するのインターフェイスもこのCLIであることが多いので、一般人の理解も完全に間違っているわけではありません。その認識は概ね正しいといえるでしょう。

シェルを自在に操るには、コンピュータへの深い理解が必要になることが多いからです。

そして、ここで言うハッカーとはコンピュータへの深い理解と知識を有した人のことを言います。決してコンピュータを使って悪事を働く人を意味するわけではありません。

コマンドと言うものは、たった1つでも多くの処理をこなしてしまうため、一見して簡単ですが、危険な面もあるということを覚えておいてください。

この点、システムやシェルへの理解がないと、間違ったコマンドを実行して大きな過ちを犯してしまう危険があります。

そして、その範囲は、OSの核であるカーネルとやり取りするだけあって、あまりに膨大です。

ただし、知っておくべき膨大な知識を前に、失敗するのを怖がっているだけでは何も始まりません。

私の考えとしては、コマンドの危険性やそのための知識はとりあえず脇に置いておき、まずはコマンドを叩いてみる、そして、それを繋げてみることから始めましょう。

確かに、失敗するかもしれません。いや、多分するでしょう。しかし、地雷を踏めば踏むほどシステムに詳しくなれると私は考えています。

例えば、私は、Arch LinuxというOSを使っていますが、Arch Linuxには山ほど地雷があります。ただ、それを踏んだからこそ、少しはコンピュータ(ここではLinux)に詳しくなったのも事実だと感じています。

さて、失敗を恐れず、いや、失敗を当然の前提として先に進むことにしましょう。

manを読むタイミング

manというマニュアル(ドキュメント)コマンドがあります。ここでmanを読むタイミングについて少しばかり紹介します。

あなたは、何かを始めようとした時、まずmanを実行することを考えますか?

ほとんどの人は入門段階では、まずGoogle検索を行うと思います。

そして、それは概ね正しい判断です。

manはつまらない事が多いし、Exampleにも対応していないことが多いからです。

このような理由から、私はmanを読むタイミングは入門を終えた後のほうが良いと考えています。

CLIに慣れているユーザーは最初にmanを読む人も多いのですが、この記事の読者対象は、あくまで初学者であり、入門者です。

したがって、いきなりmanを読むことはオススメしませんし、また、manを読むことがすなわち崇高であることにも結びつきません。

学び方は人それぞれですし、また、タイミングも人それぞれです。

自分のレベルに合ったやり方を選択していきましょう。決して背伸びする必要はありませんし、見栄を張る必要もありません。

ただし、manは非常に役立つコマンドでもあります。面白くなってきたと感じたら読んでみることをオススメします。これは多くの熟練者達も皆同じだと思います。

シェルの種類

シェルスクリプトを作成するにあたって、まず使用するシェルを決める必要があります。シェルには多くの種類があり、ハードやOSによって搭載されているものが異なります。それぞれに得意不得意があるからです。

例えば、ashというシェルはbusyboxというコマンドパッケージに入っており、小さなハードに組み込まれることが多いシェルです。それだけにミニマムですが、パワーに欠けるかもしれません。bashzshでは直前に実行したコマンドなどを!!から使えますが、ashでは使えなかった記憶があります。

ash

$ sudo pacman -S busybox
$ busybox ash
$ echo {直前のコマンド}
$ echo !!
!!

bash

$ echo {直前のコマンド}
$ echo !!
echo {直前のコマンド}

Arch Linuxのbaseパッケージにbashシェルが入っています。bashはLinuxで最も普及しているシェルです。また、shというものもありますが、実際この中身がbashに置き換えられることも多いです。

私が好んで使用しているシェルはzshと言います。これはデフォルトのTab補完が便利であることからとても人気があります。Arch Linuxのインストールディスク(ブートディスク)にも採用されています(ただしbaseには入っていない)。

なお、私は最近、fishというシェルも使っています。fishは補完の他、カラフルなインターフェイスと構文に力を入れているシェルです。fishは他のシェルとの互換性を損なう場面も多いですが、構文の解読が分かりやすいというメリットがあります。

ここで、当サイトでは主にzshを用いてシェルスクリプトを解説していきます。

理由としてはzshの使用者がそこそこいることと、パスの記述がbashよりも分かりやすいからです。

例えば、bashzshで実行するシェルスクリプトから絶対パスを取得する記述は以下のようになります。

bash

#!/bin/bash
f=$0
d=$(cd $(dirname $0);pwd)
l=$(cd $(dirname $0);cd ..;pwd)

zsh

#!/bin/zsh
f=$0
d=${0:a:h}
l=${0:a:h:h}

また、このような事情から以下のようなコマンドにも違いが出てきます。

$ echo https://syui.gitlab.io/shellscript
$ echo !$:t
shellscript

好みの問題にもなりますが、私はzshのほうが好きですね。

シェルスクリプトの作成

シェルスクリプトの作成は、通常、シバンから記述します。

シバン(shebang)というのは、シェルに当該テキストを読み込む実行コマンドを教えるために記述するもので、一般的には#!/path/to/fooという形で実行ファイルの絶対パスを記述します。

例えば、zshでシェルスクリプトを作成したければ、#!/bin/zshのように記述します。コマンドの絶対パスはwhichコマンドなどから調べることが出来ます。

$ which zsh
/bin/zsh
or
$ type zsh
zsh is /bin/zsh

また、envコマンドがあり、これはシバンにするパスを置き換える事ができます。つまり、#!/bin/env zshのような記述ができます。

$ which env
/bin/env

> #!/bin/env zsh

envを使用するメリットはシェルのインストール先が環境ごとに異なる場合です。例えば、zsh/bin/zsh/usr/bin/zshにインストールされている環境があったとして、envで記述することでどちらの環境でも動作します。

ただし、envはインタプリタにコマンドライン引数としてオプションを渡すことができないなどのデメリットがあり、特別な事情がない限りenvの使用は推奨しません。

シバンの後は、コマンドを記述すればシェルスクリプトの完成です。ここではt.shという名前で保存します。

t.sh

#!/bin/bash
echo "hello world!"

後は実行権限を渡し、t.shファイルを実行します。ファイル権限については後述しますが、xは実行権限を意味します。

$ chmod +x t.sh
$ ./t.sh
hello world!

他にもシェルスクリプトの実行には様々な方法があります。また、zsh -cなどで実行するコマンドは設定ファイルが使われないので、シェルスクリプトを書く際に便利なことがあります。シェルスクリプトを書く時、sourceなどで環境変数などを読み込むことがあり、それによってコマンドの挙動が変化することがあるので、その回避策として使えます。

$ bash t.sh
$ zsh -c ./t.sh

シェルを指定して実行した場合、ファイル自体は指定したシェルが使われますが、テキストの中身はシバンで指定したシェルが使われます。

t.sh

#!/bin/zsh
echo $SHELL

試しに上記をbashで実行してみましょう。結果はシェルスクリプト内に書いたコマンドを実行しているシェルになります。

$ bash t.sh 
/bin/zsh

ファイルのパーミッション

ファイルのパーミッション(権限)には以下の様なものが一般的です。ディレクトリも厳密にはファイルの一種なのでパーミッションがあります。基本的にはx=実行, r=読み込み, w=書き込みを意味します。

# タイムスタンプの書き換え
$ touch -t 000001010000 *

# パーミッションの確認
$ ls -l
4.0K -rwxr-xr-x 1 syui staff  35  1  1  0000 t.sh

# パーミッションの付与(すべてのユーザーはa+)
$ chmod a+rwx t.sh
4.0K -rwxrwxrwx 1 syui staff  35  1  1  0000 t.sh

# パーミッションの削除
$ chmod a-rwx t.sh
4.0K ---------- 1 syui staff  35  1  1  0000 t.sh

# 実行権限のみ付与
$ chmod +x t.sh
4.0K ---x--x--x 1 syui staff  35  1  1  0000 t.sh

ここでchmodコマンドはa+がすべてのユーザーを対象にします。u+がユーザーでg+がグループ, o+がその他です。指定しないとユーザーu+になります。

ディレクトリのパーミッションを変更したければ、以下のように-Rを使います。

$ chmod -R +x .

パスについて

基本的なパスの概念は絶対パスと相対パスがあります。絶対パスはルートディレクトリから数えたパスで、相対パスは現在地から数えたパスです。

以下、具体例を示します。

  1. 現在場所 : ~/git/shell/
  2. 絶対パス : $ /home/syui/git/shell/t.sh
  3. 相対パス : $ ./t.sh

そして、パスを通すという言葉があります。この言葉は環境変数であるPATHにパスを追加する事を意味します。パスを通すと、そのディレクトリ内にあるファイルはパスの記述なしに実行できるようになります。

本当にパスの記述が不要になるか各々試してみましょう。ここではt.shがある~/git/shellをPATHに追加することにします。

$ pwd
/home/syui/git/shell

$ echo $PATH
/bin:/usr/bin

$ export PATH=$PATH:`pwd`

$ echo $PATH
/bin:/usr/bin:/home/syui/git/shell

$ cd

$ pwd
/home/syui

$ t.sh
hello world!

様々な記述

上記ではホームディレクトリを~/home/syuiで表しています。このように、ホームディレクトリは~に省略できます。他にも様々な省略記法がシェルには用意されています。これはシェルによって変わってきますが、基本的なものは大体同じです。

$ echo ~
/home/syui

$ echo ^
t.sh

変数

シェルには最初から定義されている変数が幾つかあります。

# 現在使用しているシェルを調べる
$ echo $SHELL

ここで、変数の使い方は代入及び出力で理解できると思います。使用した文字列の先頭に$を付けると変数を使えます。また、他の文字列と連結することによって違う意味になることがあるので、確実な変数の使用のためには${x}というように括弧でくくるとより確実です。この辺はケースバイケースで考えましょう。必ずしも括弧に入れる必要はありません。なお、exportコマンドは省略できます。ただし、exportコマンドで代入することでグローバル変数にすることができ、環境が変わっても参照できるようになります。より確実に参照したい場合はexportで代入するのが良いかもしれません。

# 代入
$ export a=foo
or
$ a=foo

# 出力
$ echo $a
foo

ここで、代入というのは中身を書き換えるようなイメージです。例えば、atestを代入する場合、$ a=testというコマンドを実行することになります。実行すると変数aの中身はtestに変わります。変数というのは、このように中身を自在に書き換えて使うことが出来ます。何が便利なのかというと、何度も利用する事ができる点です。

例えば、testという文字列を10回出力したいとします。この場合、わざわざtestを10回書くよりも変数に入れて使用した方が効率が良くなります。

#!/bin/zsh
a=test
for ((i=1;i<=10;i++))
do
	echo $a
done

なお、以下のように指定回数コマンドを実行することもできます。

$ export a=test
$ zsh -c "repeat 10 echo $a"

改行を含めた変数

ここはシェルスクリプトのマハリどころなのですが、変数には改行を含めることができます。

ただし、それを参照する場合は、ダブルクオーテーションで囲まなければならないことがあります。

$ echo -e "test\nshell"
test
shell

$ a=!$

$ echo $a
testshell

$ echo "$a"
test
shell

もちろん、ダブルクオーテーションで囲まなくても改行が含まれる場合もあり、この辺はどのような理由で含まれなかったり、含まれたりするのかよく分かりません。よって、確実に改行を含めたい場面では、ダブルクオーテーションを使うようにしています。

文字列比較など

文字列比較や変数が空かどうか調べるにもダブルクオーテーションが必要になることがあります。以下は引数があればそれを出力する内容になっています。もし引数がなければerrorを表示します。

t.sh

#!/bin/zsh
if [ -n "$1" ];then
	echo $1
else
	echo error
fi
$ ./t.sh 
error

$ ./t.sh good!
good!

シングルクォーテーションとダブルクオーテーション

ダブルクオーテーション"とシングルクォーテーション'の使い分けについてです。

基本的にはダブルは変数を使うことが出来ますが、シングルは変数が使えません(書き方にもよりますが)。

$ a=test

$ echo '$a'
$a

$ echo "$a"
test

また、ダブルの中でダブルを使いたい場合などは手前に\を記述することで解決できます。

$ echo "\"$a\""
"test"

私は、そのままの文字列を使いたい場合、シングルを使うようにしています。それ以外ではダブルを使うことが多いです。

ちなみに、このようなテクニックは変数に代入するときにも使えます。

シェルスクリプトの文法

シェルスクリプトの文法は他のスクリプト言語に比べて非常に簡素で、かつ馴染みにくいものかもしれません。

ただ、一つのコマンドとして捉えると分かりやすく、そして、納得できるものでもあります。

言語的には推測が難しい側面がありますが、コマンドやコマンドオプション的には推測が可能です。

そのあたりを意識して、シェルスクリプトで使われる一般的な文法表現について見ていくことにします。

if 文の使用例

#!/bin/zsh

# コメント
a=0
if [ $a -eq 0 ];then
	echo "ok."
fi

計算や比較の条件式には[]を使います。注意点としてスペースが必要になります。これらも一つのコマンドです。-eqは同じ数値であるか比較するオプションです。

ここで、;の意味はコマンドの終わりや改行を意味します。thenは条件の終わりを意味します。つまり、以下のように書いても同じ内容になります。ちなみに、fiif文の終わりですね。

#!/bin/zsh

a=0
if [ $a -eq 0 ]
then
	echo "ok."
fi

また、単純に条件処理をしたい場合は、括弧を使わずifのみで判定することもできます。

例えば、コマンドが成功した場合の処理は以下のようになります。つまり、条件にif ~以降に記述されるコマンドの成否を設定しているわけです。

#!/bin/zsh

if echo 0 | grep 0;then
	echo "$?"
fi

次に、条件の追加elifや条件に当てはまらない場合の処理elseを書くことができます。注意点として、文字列比較は必ずダブルでもシングルでも良いので、クォーテーションを付けなければなりません。

#!/bin/zsh

str=test
if [ "$str" = "foo" ];then
  echo "foo"
elif [ "$str" = "bar" ];then
  echo "bar"
else
  echo "unset"
fi

条件を反転させる場合は、大抵、!を付けることで解決できます。この例では、aでない時に条件が満たされます。

if [ "a" != "b" ];then
	:
fi
if !echo a|grep b;then
	:
fi

fish-shell

https://fishshell.com/docs/current/index.html

ここではfishの情報も合わせて記述します。

まず、fishは他のshellとの互換性を完全に捨ててる。考慮もあんまりしてない感じだけど、それ故に可読性は良いと思う。が、shellの強みって一貫した書き方だったりするわけで、その点では触っていてめんどくささはかなりある。まあ、これをshellと見ること無く、アプリとか言語の一つとかとして見る場合は普通なんだけど、そんな感じです。これは何が問題なのかというと、例えば、他のアプリがshell変数なんかをあてにしてたりする場合、fishを使うことによって思わぬ動作不良を招くことも多いため、あくまで嗜好品とか、楽しんで使うアプリとしてみるほうが良いのかもしれません。

fishはあまり設定書かなくても使える。補完とかプロンプトだったり。あと、プラグインが読みやすいし、オリジナルの設定を書きやすい、改変しやすい感じ。但し、補完も書かないと不便なところは多くて、それは例えば特殊文字の扱いとか色々なんだけど、zshのほうが便利な部分もある。

最初に嵌りそうな点は以下。

# export a="word"
set a "word"

# a=`echo word`
set a (echo word)

# if which ls;then echo ok ls;fi
if
	which ls
end

# case $1 in
switch (echo arvg[1])
	case test
		echo ok
	end
end

# function a (){echo a}
# functionは付けなくてもいいが、内容によってはバッティングするため動作不良を引き起こす可能性あり
function a
	echo a
end

# alias v="vim"
alias v="vim"
# aliasで書いた文はfunctionで再定義されるのでそのまま書いてもいい
$ type v
function v
	vim
end

# case $OSTYPE in
# fishではいくつかの環境変数が設定されていない
switch (uname)
	case Darwin 
		echo mac
	case Linux
		echo linux
	case '*'
		echo other
	end
end

# . ~/.zshrc && exec $SHELL
# fishは設定を再度読み込むより再起動したほうがいい事が多い
exec fish

いろんなものをインストール。よく使うのはz, fzf, ghq, hub, tmuxなのでそれらと連携するプラグインなど。

$ curl -Lo ~/.config/fish/functions/fisher.fish --create-dirs git.io/fisher
$ fisher fzf z edc/bass omf/thefuck omf/theme-bobthefish decors/fish-ghq

fishでは~/.config/fish/config.fishに書いたバインドは有効になる場合とならない場合があり、基本的には有効になるが、~/.config/fish/functions/以下に置いたfish_user_key_bindings関数が無効になる。つまり、fish_user_key_bindings関数に書くのが順当であり、かつその関数ファイルはfunctions/に置くのが一般的。内容によってはcommandline -f repaintを入れないとプロンプトでEnterを押す必要が出てくるので必要な場合あり。

~/.config/fish/functions/fish_user_key_bindings.fish

function __fish_cd_up
	cd ..
	ls -slFa
	commandline -f repaint
end

function fish_user_key_bindings
	bind \cf peco_select_ghq_repository
	bind \ck __fish_cd_up
	bind \cs __fzf_find_file
	bind \cx __fzf_find_and_execute
	bind \ec __fzf_cd
	bind \eC __fzf_cd_with_hidden
	bind -M insert \eD __fzf_cd_with_hidden
end

補完などはインストール時についてくるファイルを利用したりとか色々。PATHを通してもいいし、補完を作成したり、~/.config/fish/completionsに置いてもいい。

# https://github.com/github/hub/blob/master/etc/hub.fish_completion
$ brew install hub
ls /usr/local/bin/share/fish/completions/
$ fish_update_completions

$ hub <Tab>
	alias (show shell instructions for wrapping git)
	browse (browse the project on GitHub)
	ci-status (display GitHub Status information for a commit)
	compare (lookup commit in GitHub Status API)
	create (create new repo on GitHub for the current project)
	fork (fork origin repo on GitHub)
	pull-request (open a pull request on GitHub)

tmuxとの連携は~/.tmux.confにset-option -g default-command "reattach-to-user-namespace -l fish"を書くことでvimでのクリップボードとか色々な不都合が起こりにくい。

config.fish

if test $TERM != "screen"
 tmux new-session
 and exit
end

zとの連携は以下のような感じ。j -lで取得したりできるので、それを元に関数を書く。キーバインドのC-jで移動。

~/.config/fish/config.fish

set -U Z_CMD "j"
set -U Z_DATA "$HOME/.z"

~/.config/fish/functions/fish_user_key_bindings.fish

function peco_z
 set -l query (commandline)

 if test -n $query
 set peco_flags --query "$query"
 end

 j -l | peco $peco_flags | awk '{ print $2 }' | read recent
 if [ $recent ]
 cd $recent
 commandline -r ''
 commandline -f repaint
 end
end

function fish_user_key_bindings
 bind \e peco_z
end