シェルとは?
「シェル」=「shell」=「殻」です。
LinuxのOS(カーネル)本体は、ユーザからの操作機能をもっていないためユーザの操作インターフェースの役割を担うのがこの「シェル」になります。
イメージ図のとおり、殻(Shell)に覆われているイメージのため「シェル」と呼ばれています。
よく「シェル」が「貝殻」の絵で表現されているのを見かけますが、日本語だと「貝殻」というよりは「殻」のニュアンスの意味でしょうね。
ユーザはターミナルでコマンドを実行することにより、OSの操作ができます。
もう少し説明すると、OSには「システムコール」というものが用意されていて、コマンドを実行すると、裏側でこのシステムコールを呼び出してOSを操作できるようになります。
コマンドを実行して操作する環境を「CLI」(Command Line Interface)といいます。
日本ではGUI(Graphical User Interface)と対比して「CUI」(CommandLine User Interface)と呼ばれることも多いですね。
このCLI環境のおかげで、近年のCI/CD、自動化の土台ができました。
GUI環境はコンピュータ初心者でも操作がしやすいというメリットはありますが、自動化においては大きな欠点となっております。
一方、LinuxのようにCLIで何でもできる環境は操作はとっつきにくいですが、すべての操作がコマンドで完結することができます。
「コマンドで実行できる」状態であれば、あとはシェルスクリプトでもプログラムでも作ればあとは処理に従って自由自在に動かすことができます。
これがGUIだとマウス操作を自動化しなければならず、RPAや画像処理技術の向上である程度自動化できるようにはなってきましたが、CLIと比較するとまだまだです。
今では当たり前のように使ってるシェルですが、実は現代の技術の土台となる偉大な技術だったのです。
以下は、シェルがコマンド(命令)の入力を受け付けている様子です。
[root@localhost ~]#
※ターミナルソフトは「TeraTerm」を利用しています。sshでログインするとシェルが起動します。
この「コマンドを受け付け」している状態を表す文字を「コマンドプロンプト」と呼びます。
「コマンド」とは、シェルに命令を与える文字列で、ファイル、ディレクトリを表示させる「ls」コマンドや、ディレクトリを移動する「cd」コマンドなの、様々なコマンドが用意されています。
Windowsでも「コマンドプロンプト」を起動させると、黒い画面が出てきますがあれもコマンドを実行できるアプリケーションです。
(残念ながらWindowsのコマンドプロンプトはシェルと比較して、機能が充実していないため、あくまでもおまけ程度のものです)
シェルの種類
大きくわけてBourn shell(sh)系と C shell(csh)系の2種類があります。
- sh系
- Bourn shell、bash、Korn shell(ksh)、zsh
- csh系
- C shell、tcsh
各シェルの特徴
■Bourne shell
最初に登場したのがBourne shellです。(さらにさかのぼると、元祖と呼ばれているThompson Shellもありますがここでは省略します)
Bourne shellは UNIX Version7のデフォルトシェルとして採用されました。
シェルの原型ともいえる大部分の機能がこのBourne shellで誕生しています。
<機能>
- シェルスクリプト
- 記述したファイル名をそのままコマンドとして実行できます。
- 入出力のリダイレクト
- パイプ機能
- ヒアドキュメント「<<」
- バッククォートによるコマンド「
~コマンド~
」 - 標準出力、標準エラー出力
- 環境変数
- など
ただ、現在のシェルと比較して対話機能がまだまだ足りず、後年様々なシェルが誕生することとなりました。
最近のLinuxとかでは使いたいと思っても利用する機会は無いですが、昔のUNIX系のOSに搭載されているBourn shellを使ったことがある人はあまり積極的には使いたくないシェルだと思います。
■C shell
一方C shellもBourne shellと同様にUNIX Version6の/bin/shをもとに作られました。
C shellはコマンド履歴、エイリアス、ジョブなどの機能を始めて取り入れたシェルで、その機能は今ではどのシェルでも標準機能となっています。
C shellによってコマンドラインとしての操作性が向上しました。操作のスピードアップに大きく貢献したのがC shellです。
ここでC shell人気が出てきました。
そして、シェルスクリプトの文法はBourne shellとは異なっており互換性がありません。
Bourne shellと比較するとC言語に似せた文法となっており、当時はBourne shellよりわかりやすいと人気でした。
■tcsh
tcshはC shell互換でさらに機能が強化されたシェルです。
tcshの機能として大きかったのが、実行可能なコマンドの保管機能ですね。
途中まで文字を入力してtabを実行すると、実行可能なコマンドの文字が表示される機能です。
UNIX系のOSでは、この機能のためにtcshがよく使われていました。
■bash
そして、ついにbashの登場です。
今では立派にLinuxの標準シェルになっていていますが、Bourne shellをベースにC shell、Korn shell、tcshの機能を取り入れました。
名前のもとになっているのがBourne-again shellのとおり、Bourne shellの後継を目指して改良が加えられました。
使い勝手や操作性で好評だったkorn shell、tcshの機能も取り入れており、また過去Bourne shellのスクリプトもほぼ無修正で実行可能であることからbashさえあれば他のシェルは不要と思えるようになりました。
Linuxの誕生日とされる1991/8/25にLinus comp.os.minixのニュースグループに送信されたメールで「I’ve currently ported bash(1.08)~」という文章があります。
今ではLinuxだけでなくWindowsでも採用されて、標準の「シェル」としての立場を確保したbashですが、Linuxの初期段階からbashが採用されたのが普及の大きな原動力になったのかもしれません。
今ではLinuxだけでなくWindowsでも利用できるようになりました。
これ以降は主にbashを中心に説明していきます。
※同じbashでもDebian系とRedHat系で多少異なるかもしれませんが、CentOSのほうがなじみがあるので、RedHat系を中心に記載します。
インストールされているスクリプト
以下のコマンドで利用できるシェル一覧を表示できます。
# cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash
/bin/tcsh
/bin/csh
/etc/shellsファイルにログインシェルとして有効なシェルのパスが記載されています。
組み込みコマンド
RedHat系のLinuxではbashの組み込みコマンドは「rpm -ql bash」の一覧に出てきます。
それ以外は普通のコマンドです。
- 普通のコマンド
- シェルに含まれていないので追加でインストールが必要
- Linuxのディストリビューション、インストールオプション次第でインストールされてないコマンドがある
- 組み込みコマンド
- シェルにデフォルトで入っているコマンド
- Linuxでbashが動いていれば必ず入っている
- 必要最低限のものだけある
- 組み込みコマンドだけだとほとんど何もできない
組み込みコマンドは以下のコマンドで一覧を表示できます。
bash# help
alias、echo、export、fcコマンドなどがあります。
以外とlsコマンドはシェル組み込みでは無いようですね。
ちなみに、馴染みのあるls、cp、catコマンド等はUNIXの基本的なユーティリティとして「coreutils」パッケージとして開発、提供されています。
RedHat系のOSであれば以下のコマンドで一覧を確認できます。
bash# rpm -ql coreutils
環境変数の設定
実際に今設定されている環境変数の値を見てみましょう。
printenvコマンドを使って確認します。
# printenv
ずらずらと表示されます。
イコール(=)の左側が変数名で、右側が変数に入っている値です。
「変数」はプログラミングでも登場しますが、同じ概念です。
XDG_SESSION_ID=1
HOSTNAME=localhost
SELINUX_ROLE_REQUESTED=
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=192.168.1.1 50183 22
SELINUX_USE_CURRENT_RANGE=
OLDPWD=/root
SSH_TTY=/dev/pts/0
USER=root
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
MAIL=/var/spool/mail/root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
PWD=/
LANG=ja_JP.UTF-8
SELINUX_LEVEL_REQUESTED=
HISTCONTROL=ignoredups
SHLVL=1
HOME=/root
LOGNAME=root
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/0
_=/usr/bin/printenv
設定する場合はexportコマンドを利用して設定します。
おそらく、一番よく使うのはPATH変数だと思いますので、PATH変数で実験してみましょう。
まず、/tmp配下にシェルスクリプトを作成します。
# echo "echo 'test'" > /tmp/echo.sh
作成したら実行してみましょう。
chmodコマンドで実行権限をつけてから実行します。
# chomd 755 /tmp/echo.sh
# /tmp/echo.sh
test
うまく実行できました。
つぎに/tmp/を省略してecho.shのファイル名だけで実行してみます。
# echo.sh
-bash: echo.sh: コマンドが見つかりません
「コマンドが見つかりません」と出力されて実行できませんでした。
これがよくいう「パス(PATH)が通ってない」という状態です。
UNIXに馴染んでないと、急に「パスが通ってない」と言われても「へ?」となりますよね。
もう一度printenvコマンドで環境変数の状態を見てみましょう。
今度はgrepコマンドで、PATHの行だけ抜き出します。
# printenv | grep PATH
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
環境変数「PATH」で設定されている値が表示されました。
~/sbinとか、~/binとか「bin」っぽい名前のディレクトリパスが設定されていますね。
この「PATH」の環境変数に設定したパス配下に存在するコマンドは、フルパス指定でなくてもファイル名だけで実行可能になります。
例えば、cdコマンドが「/usr/bin/cd」と書いて実行しなくても「cd」だけで実行できるのはPATHの環境変数のおかげです。
なお、コマンドのフルPATHは以下の「which」コマンドで確認できます。
# which cd
/usr/bin/cd
それでは、さきほど作成した/tmp/echo.shを直接「echo.sh」だけで実行できるようにPATHの設定をしてみましょう。
# export PATH=/tmp:${PATH}
このコマンドを実行すると、今のPATH環境変数の値の先頭に「/tmp」のパスを1つ追加しています。区切り文字は「:」を指定します。
確認すると、/tmpのパスが追加されていました。
# printenv | grep PATH
PATH=/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
環境変数の設定をしたら、echo.shを実行してみましょう。
# echo.sh
test
今度は、「echo.sh」だけで実行できました。
これが、「/tmp配下にパスが通った状態」です。
※/tmp配下にPATHを通すのは練習時という前提です。
※通常、/tmp配下は誰でも書き込めるディレクトリですので、むやみに/tmp配下にPATHの設定をすると、悪意のあるコマンドが置かれていた場合とても危険ですので、練習以外では/tmp配下にPATHを設定しないようにしましょう。
設定ファイル
bash関連の設定ファイルは以下があります。
ログインするたびに環境変数とかエイリアスとか実行するもの面倒なので、ログインしたときはシェルを起動したときに自動的に読み込まれるファイルが用意されています。
自動的に読み込むには以下のように指定されたファイルに記載する必要がありますが、手動で読み込ませる場合は、任意の名前でOKです。
■bash関連の設定ファイル
- /etc/bashrc
- /etc/profile
- .bashrc
- .bash_profile
- .bash_logout
/etc/bashrc、.bashrcはシェル起動時に読み込まれます。
/etc/profile、.bash_profileはログイン時に読み込まれます。つまりログインしたときに起動するログインシェルが起動したときに読み込まれます。
シェル起動時には読み込まれません。
少しややこしいですが、ログインしたあとにbashを実行した場合はログインシェルではなく、ただのシェルの起動です。
さて「ログインシェル」とは何でしょうか?
/etc/passwdファイルに記載されているシェルが、ログインシェルです。
Linuxであれば、自分から指定しない限りは/bin/bashが指定されているかと思います。
参考までに、アプリケーション用のユーザであれば、/sbin/nologinと指定されていて、ログインシェルが起動しないようになっています。
試しに以下のように実験してみましょう。
「bash」を実行してシェルを起動したときは「.bashrc」だけ読み込まれて、「.bash_profile」が読み込まれてないのがよくわかりますね。
# echo "echo 'This is bashrc'" >> ~/.bashrc
# echo "echo 'This is bash_profile'" >> ~/.bash_profile
# bash
This is bashrc
#
さらに/etc配下のファイルの順番も含めてみてみましょう。
以下はechoコマンドで文字列を指定したファイルの一番下に追記しています。
echoコマンドや見慣れない記号「>>」の説明は後述します。
# echo "echo 'This is bashrc'" >> ~/.bashrc
# echo "echo 'This is bash_profile'" >> ~/.bash_profile
# echo "echo 'This is /etc/bashrc'" >> /etc/bashrc
# echo "echo 'This is /etc/profile'" >> /etc/profile
# bash
This is /etc/bashrc
This is bashrc
#
bash起動時にはまず/etc/bashrcが読み込まれるようです。
次に各ユーザの.bashrcが読み込まれます。
/etc/配下のbashrc、profileは各ユーザで共通の設定を入れておく場合に使います。
次に「su -」を実行してみましょう。
これでログインシェルを起動することができます。
# su -
Last login: Sat Sep 3 22:00:00 JST 2022 on pts/0
This is /etc/profile
This is /etc/bashrc
This is bashrc
This is bash_profile
#
このとおり、/etc/profileと.bash_profileが読み込まれました。
ただ、.bash_profileの前に.bashrcファイルが読み込まれてますね。
/etc/profileは/etc/bashrcより先に読み込まれていますが、各ユーザのホームにある設定ファイルは、.bashrcファイルのほうが先に読み込まれています。
(理由まではわかりませんでしたが実験するとこうなりました)
ユーザ作成時のテンプレート用のファイル/etc/skelフォルダにあります。
設定ファイルはどう使い分けるのか?
「とりあえずexportする環境変数は.bash_profileに入れておけばいい」とか、自分なりのルールがある人も多いのではないでしょうか?
あまり大きな違いはないかもしれませんが、先に実験したとおり.bash_profileに入れた場合はbash起動時には反映されません。
「どっちに入れてもとりあえず動けばいい」という人は、そのあたりを念頭に入れておけばよいでしょう。
参考までにデフォルトのテンプレートを見ると、以下のようなお作法はあるようです。
- .bash_profile
- exportとか環境変数を設定
- .bashrc
- aliasなどを設定
また、/etc/profileの最初にもコメントがありますね。
# /etc/profile
# System wide environment and startup programs, for login setup
# Functions and aliases go in /etc/bashrc
# It's NOT a good idea to change this file unless you know what you
# are doing. It's much better to create a custom.sh shell script in
# /etc/profile.d/ to make custom changes to your environment, as this
# will prevent the need for merging in future updates.
訳
ログイン設定のためのシステム全体の環境と起動プログラム関数とエイリアスは /etc/bashrc に入ります
自分が何をしているのかわからない限り、このファイルを変更するのは得策ではありません。 custom.sh シェル スクリプトを作成する方がはるかに優れています。
/etc/profile.d/ を使用して、環境にカスタムの変更を加えます。これにより、将来の更新でマージする必要がなくなります。
続いて/etc/bashrcに書いてあるコメントを見てみましょう。
同じようなことが書いてありますね。
# /etc/bashrc
# System wide functions and aliases
# Environment stuff goes in /etc/profile
# It's NOT a good idea to change this file unless you know what you
# are doing. It's much better to create a custom.sh shell script in
# /etc/profile.d/ to make custom changes to your environment, as this
# will prevent the need for merging in future updates.
訳
システム全体の機能とエイリアス 環境関連のものは /etc/profile に入る
自分が何をしているのかわからない限り、このファイルを変更するのは得策ではありません。 /etc/profile.d/ に custom.sh シェル スクリプトを作成して、環境にカスタムの変更を加えることをお勧めします。これにより、将来の更新でマージする必要がなくなります。
シェル変数と環境変数の違い
端的に説明すると、exportで設定するのが環境変数で、exportをつけない単なる変数がシェル変数です。
exportの場合は現在のシェルだけでなく子プロセスにも引き継がれます。
シェル変数の場合は、現在のシェルの中だけで変数の値が有効になります。
- シェル変数
- そのシェルだけで有効な変数
- シェルスクリプトを実行するときなど、シェル変数の値は引き継がれない
- 環境変数
- exportコマンドで設定
- 子プロセスにも引き継がれる
それでは実際にコマンドで動きをみてみましょう。
まずはシェル変数の動きからです。
# HENSU="This is shell hensu." ←シェル変数の設定
# echo ${HENSU} ←シェル変数の中身を表示
This is shell hensu.
# bash ←新しいシェルを起動
# echo ${HENSU} ←再度シェル変数の中身を表示
←何も表示されない
#
上記のとおりシェル変数は子プロセスには引き継がれません。
次に環境変数の動きを見てみましょう。
# export KANKYO_HENSU="This is kankyo hensu." ←環境変数の設定
# echo ${KANKYO_HENSU} ←環境変数の中身を表示
This is kankyo hensu.
# bash ←新しいシェルを起動
# echo ${KANKYO_HENSU} ←再度シェル変数の中身を表示
This is kankyo hensu. ←新しいシェルでも表示された
#
違いはわかりましたでしょうか?
exportをつけて設定した環境変数のほうは、新しいシェルを起動した後でも中身が表示されましたね。
シェルスクリプトを作って実行した場合も環境変数のほうはシェルスクリプトにも反映されることになります。
シェルスクリプトとプロセス
シェルの説明で記載したとおり、シェルの偉大な機能として、シェルスクリプトがあります。
テキストファイルにスクリプトを記述して、コマンドのように実行することが可能です。
シェルスクリプトではif文の条件分岐、for、while文のループなどが利用できプログラムとして動作します。
すぐに実行できて結果もすぐにわかるので、プログラム入門として最適です。
pythonのようにライブラリをインストールしたり、フレームワークを利用したりすることはないですが、その分プログラミングに集中できるので、プログラムを学びたい人はまずはシェルスクリプトを学ぶのも僕は良いかなと思っています。
シェルスクリプトはバージョンが合わなくてエラーが出たり、環境の問題でうまく動かなかったり、はまるポイントがほとんどないので、挫折せずプログラミングを楽しめるというメリットがあります。
せっかくプログラムを始めたのに、はまって挫折してしまう人をよく知ってますので、いつももったいないなーと思っています。
(でも「シェルスクリプトを勉強しても無駄」という考えの方が主流かもしれません)
それでは、試しにシェルスクリプトを作って動かしてみましょう。
ソース
# vi test.sh
--------- test.sh ------------
#! /bin/sh
echo "befor sleep"
sleep 60
echo "after sleep"
------------------------------
上記のようにtest.shファイルを作成します。
「sleep 60」で、60秒間何も動かない処理を入れます。
その前後に「echo」コマンドで文字列を出力します。
実行
# ./test.sh 【Enter』
「./」をつけてtest.shを入力して実行します。
「./」はカレントディレクトを表していて、カレントディレクトリにある「test.sh」という名前のシェルスクリプトを実行しています。
実行結果
# ./test.sh
befor sleep
実行した直後は上記のように「befor sleep」が出力されていると思います。
ここで上記のとおり出力されない場合は、test.shの記載内容がどこか間違っている可能性がありますので、もう一度確認してください。
実行した後、別のターミナルを起動して以下のコマンドを実行しましょう。
もしターミナルを1つしか実行できない環境の場合はスキップしてください。
プロセスIDについては、別の章でも詳しく説明しますので、ここではシェルスクリプトを作って実行できるというところがわかれば十分です。
プロセスを確認
# ps -ef | grep test
root 20619 1925 0 22:24 pts/0 00:00:00 /bin/sh ./test.sh
psコマンドでプロセスを見てみましょう。
grepコマンドで「test」という文字列のプロセスだけフィルタして表示しています。
psコマンドで出力された場合、プロセスとして存在しているということになります。
grepで特定の場所だけ表示しているため、ヘッダ部分が表示されていませんが、各項目のヘッダ情報は以下のコマンドで確認できます。
# ps -ef | head -1
UID PID PPID C STIME TTY TIME CMD
出力の意味
- UID: 実行ユーザ。今回はrootユーザで実行しているためrootと表示されています。(※あまりテストで特権を持っているrootユーザを使うことは推奨されていませんが、この環境は検証環境のためrootユーザで実行しています)
- PID: プロセスIDのことです。test.shのプロセスIDは20619でした。
- PPID: 親プロセスIDのことです。test.shを起動した親プロセスのIDは1925です。「親プロセス」=「スクリプトを実行したシェル」になります。
- C: CPU使用率。sleepの場合はCPUを使用しないので0になります。
- STIME: 開始時間。
- TTY: ターミナル番号
- TIME: 実行時間。
- CMD: 実行コマンド名(プロセス名)
しばらくするとtest.shを実行したターミナル上で「after sleep」という文字列が追加で出力されていると思います。
sleep行の下に書いてましたので、「sleep 60」の処理が完了したため表示されました。
# ./test.sh
befor sleep
after sleep
#
このようにコマンドを組み合わせて、まとめて1つのコマンドとして実行できるのがシェルスクリプトの大きな魅力です。
コメント