Linuxの基本(その5)シェルとシェルスクリプト

コマンドの実行は「シェル」を介して行なっています

「シェル」には「対話的なつかい方」と「シェルスクリプトを書いて実行するつかい方」があります

目次
  1. シェルについて
  2. Bash環境のカスタマイズ
  3. シェルスクリプト
    1. 作成と実行の基本
    2. 条件分岐
    3. ループ処理
    4. select文
    5. 関数をつかう
    6. デバック
  4. カッコ早見表w
  5. 関連投稿

シェルについて

プログラムを実行するにはカーネル(OSの中核)とやり取りする必要があります
利用者がカーネルとやり取りするには「シェルというソフトウェア」が必要です 
「シェル」は「利用者とカーネルの仲介役」として「対話的(インタラクティブ)」に「利用者の要求」に対して「必要な処理を実行」します

「シェル」がコマンドを実行すると「プロセス」が立ち上がります
複数の「シェル」を起動することができ、新しく起動したシェルは「現在のプロセス」の「子プロセス」になります

シェルのおもな役割

  • シェルコマンドの実行
  • プログラムの起動・停止・切り替え
  • ファイルの操作
  • コマンドの入力履歴の保存
  • コマンドの補完

「カーネルとシェル」「プロセス」についての参考

シェルの種類について

「bash・sh・zsh・ksh・csh・tcshなど」さまざまな種類があり「bash」が多くのLinuxディストリビューションで標準的につかわれています

*種類によって多少の差異があります(設定ファイル名が異なります)
ここでは「bash」をつかいます

変数には「シェル変数」と「環境変数」があります
「利用する値」を「変数に格納」し「値を利用するとき」に「変数を参照」します

シェル変数」は「変数を宣言したシェル」でのみ参照でき「子プロセス」からは参照できません
環境変数」はすべてのプロセスで有効な変数で「子プロセス」からも参照できます

コマンドラインの入力内容
変数名=値シェル変数の定義
export 変数名=値環境変数の定義
echo $変数名値を表示
bash対話型シェル子プロセス)の起動
*シェルには「ログインシェル」と「対話型シェル」があります
詳細はあとで
exitシェルの終了
unset 変数名変数の削除
setシェル変数と環境変数両方の一覧
env環境変数の一覧
printenv 変数名指定した環境変数のみ表示
echo $変数名でもOK

(注意)変数の宣言では「=」の前後にスペースをあけない
*スペースをあけるとコマンドとして解釈されてエラーになります

代表的な環境変数

変数名内容
PATHコマンドなどの置かれているパス
*PATH変数に「/bin」などが登録されているから、コマンド名だけで実行できます
HOME現在の利用者のホームディレクトリ
PWDカレントディレクトリ
PS1プロンプト(入力まちで表示されるカーソルの前の短い文字や記号)
LANG使用する言語

(余談)PATHという環境変数について

エイリアス(別名)について
「aliasコマンド」は、長いコマンドなどに「別名をつけて」入力を簡素化できます
*「シェル変数」と同じく「aliasを設定したシェル」でのみ参照できます(つねに利用するには、「対話型シェル起動時にも読み込む設定ファイル」に記載します)

コマンドラインの入力内容
alias 別名='コマンド'コマンドに別名をつける
alias 別名コマンドの確認
\(または¥)別名別名を一時無効にする
unalias 別名別名の削除
unalias -a別名をすべて削除

Bash環境のカスタマイズ

「環境変数の定義」や「エイリアス(別名)の登録」は、ターミナルを終了するとリセットされます

しかし
シェル起動時に読み込む「特別な設定ファイル」に「Bash環境を記載する」ことで、ユーザ専用の「環境変数の設定」「エイリアス(別名)の登録」が永続的になります
ようするに、「Bash環境のカスタマイズ」ができます

ややこしいのはw
「特別な設定ファイル名」が複数あり「ログインシェルか?対話型シェルか?」「対象が全ユーザか?各ユーザか?」で「記載する設定ファイル名」や「設定ファイルを読み込む順番」がことなります
*ディストリビューションでも差異があります

「ログインシェル」と「対話型シェル」について

ざっくりw
ユーザがログインした後に「最初に起動されるシェル」が「ログインシェル」
ログイン後「bashで起動されるシェル」が「対話型シェル」

「ログインシェル」になる場合
*「ログインシェル」のおもな役割は、各ユーザの環境を「シェル」にセットすることです

  • su -ユーザ名 でシェルを起動したとき
  • bash --login でシェルを起動したとき
  • sshでログインしたとき

「対話型シェル」になる場合

  • bash でシェルを起動したとき
  • su ユーザ名でシェルを起動したとき
  • デスクトップ環境でターミナルを起動したとき

「ログインシェル」「対話型シェル」の確認方法

echo $0
「-bash」と表示されたときは「ログインシェル」
「bash」と表示されたときは「対話型シェル」

話しがそれますが…デスクトップ環境について
「CUI」と「GUI」のちがい

「CUI」は「キーボード」ですべての操作をする画面
「GUI」は「WindowsやMac」のような「グラフィカルなデスクトップ環境」です
Linuxはコマンドを入力して実行する「CUI」が基本ですが、もちろん「グラフィカルなデスクトップ環境(GUI)」も使えます

(余談)「LinuxのGUIを実現しているアプリケーション」が「X Window System」で「Linuxの統合デスクトップ環境」では「GNOME」「KDE」が有名です

「Linuxのデスクトップ環境」でターミナルを起動したときは「対話型シェル」になります
(余談)Macでターミナルを起動したときは「ログインシェル」なので、???になりました^^;

LinuxでGUI環境がインストールされているときは「GUI」と「CUI」を切りかえできます

  • UbuntuでGUIからCUIに切りかえるとき、Ctrl+Alt+F1Ctrl+Alt+F7で元のモードに戻る
  • CentOSで切りかえるとき、init 3(CUIモード)・init 5(GUIモード)

話しを戻し、「シェル起動時に読み込む設定ファイル」について

~/.bashrc」は「環境変数」と「エイリアス」どちらも読み込みます

「ログインシェル」でないときは「.bashrc」を読み込み、ログインシェルのときは「.bash_profile」内で「.bashrc」を読み込むように設定されています

「~/.bashrc」に書き込む
標準出力で設定ファイルに追記する(viで編集してもいい)

# 環境変数
echo "export PATH=$PATH:/追加するディレクトリ(絶対パス)" >> ~/.bashrc
# エイリアス
echo "alias 別名='コマンド'"  >> ~/.bashrc
# 現在のシェルで実行する
source ~/.bashrce

*「sourceコマンド」設定ファイルを実行(読み込む)する理由は、設定ファイルは起動時にしか読み込まないからです

「~/.bashrc」は便利ですが、とはいえ、設定ファイルの詳細について💦

「ユーザ用」設定ファイル
すべてのファイルは、ユーザのホームディレクトリ直下(~/)にあります

~/.bash_profile
ログイン時・読込順 2
~/.bash_login
ログイン時・「~/.bash_profile」が存在しないときに読み込む
~/.profile
ログイン時・「~/.bash_profile」「~/.bash_login」が存在しないときに読み込む
~/.bashrc
ログイン時・読込順 3
対話型シェル起動時(エイリアスの設定)・読込順 1

「全てのユーザが対象」設定ファイル
(/etc/)ホームディレクトリ直下にあります

/etc/profile
ログイン時・読込順
/etc/bashrc(RedHat系のみ)
ログイン時・読込順 4
対話型シェル起動時(エイリアスの設定)・読込順 2
/etc/bash.bashrc(Debian系のみ)
ログイン時・読込順 1.5
対話型シェル起動時(エイリアスの設定)・読込順 0.5

ユーザ用その他の設定ファイル

~/.bash_logout
ログアウト時に実行することを記載
~/.bash_history
コマンドの実行履歴を保存

シェルスクリプト

あらかじめ「実行したいコマンド」をファイル(シェルスクリプト)に書き「まとめて実行」します
*シェルスクリプトの書き方はシェルの種類で差異があります
cat /etc/shells」で「シェル一覧」が表示されます(「bash」をつかいます)

作成と実行の基本

「シェルスクリプトのファイル名の末尾」には「.sh」をつけます
ファイルの先頭にシバン「#! /bin/bash」を書きます(処理するシェルを指定するための記述)

シェルスクリプトの実行方法

  • 「source」コマンドを使う source hoge.sh (「source」は「.」でもいい. hoge.sh)
    シバンは無視され、現在の環境下(bash)で実行されます(主に設定ファイルの読み込みにつかう)
  • bashシェルを起動して引数に指定する bash hoge.sh
    シバンは無視され、新しいbashシェル環境で実行されます
  • ファイルに実行権をつけて直接実行 ./hoge.sh
    シバンが読み込まれ、新しいシバンで指定した環境下で実行されます

シェルスクリプトファイルのパーミッションについて

初期設定で最初にファイルを作成した場合は、実行権限はついていません
ファイルに実行権をつけて直接実行するには
chmod a+x ファイル名」や「chmod 755 ファイル名」で権限を変更します

*3つの方法すべてに「読み取り権限」は必要です

シェルスクリプトファイルの中身(基本)

処理の終了
exit 0(成功)
exit 1(失敗)
このあとに書いた処理は実行されない
変数の定義
変数名='文字列' : 文字列を変数に格納
変数名=`コマンド`または変数名=$(コマンド):コマンドの実行結果を変数に格納
変数の表示
echo $変数名
配列の作成
配列名=('文字列1' '文字列2' '文字列3')
配列の表示
echo ${配列名[@]} : 値をすべて表示
echo ${配列名[0]} : インデックス「0」の値を表示
echo ${!配列名[@]} : インデックスを表示
echo ${#配列名[@]} : 要素の数を表示
配列に値を追加・配列の値を削除
配列名[インデックス]='追加する値' : インデックス番目に値を追加
unset 配列名[インデックス] :インデックス番目の値を削除
引数の表示
echo $1 $2…$n : $1が第一引数・ $2が第二引数 ・$n(n番目の引数)
echo $0 : ファイル名を表示
echo $@ : すべての引数を表示
echo $# : 引数の数を表示
$?:直前に実行したコマンドの戻り値(0は成功)
標準入力から値を受け取る
read 値名:
受け取った値の処理は$値名(例えば変数に格納、変数名=$値名
read -p '説明文' 値名 : 説明文をつける
read -s 値名 : シークレットモード
read 値名1 値名2 値名3:複数の値を受け取ることも可能
(配列で受け取る場合は-aオプションをつける) 
結果をファイルに標準出力する
> ファイル名 : 上書き
>> ファイル名 : 追加
コマンドの区切り
; : 複数のコマンドを順に実行したいときはコマンドの間に「;」を挿入する。
*コマンドの出力結果を次のコマンドに渡さない
(余談)「| 」はコマンドの出力を次のコマンドの入力として渡します
シェルスクリプトを実行する
. : 後ろに指定したシェルスクリプトを実行します
プロセスは現在のシェル上で動作します(新たなプロセスは作成されません)

「unsetコマンド」は変数や関数そのものを削除します
unset [オプション] 変数や関数の名前

オプション内容
-f関数名として扱う
-v変数名として扱う

引用符の種類と内容

種類内容
' 'すべて文字列とみなされる
" "変数は展開される
` `コマンド結果が展開される

シェル変数はすべて文字列扱いであるため、数値計算のためには専用のコマンドをつかいます
四則演算(計算)を実行して、結果を文字列に展開する
「$((….))」 または「$(expr ….)」をつかいます
*変数を利用する場合
$((num1+num2)) :「変数の前の$」は不要
$(expr $num1 + $num2) :「変数の前に$」が必要

内容書き方
足し算(1+1)$((1+1))
$(expr 1 + 1) *+の前後のスペースをあける
引き算(1-1)$((1-1))
$(expr 1 - 1)
掛け算(1*1)$((1*1))
$(expr 1 \* 1) *「\(エスケープ)」が必要
割り算(1%1)
小数点以下は切り捨て
$((1\1))
$(expr 1 / 1)
余り(1%1の余り)$((1%1))
$(expr 1 % 1)

「bcコマンド」は、対話的に計算を行うコマンドです
length=数値:有効桁数を指定
scale=数値:小数点以下の有効桁数を指定
bc -l : 標準数学ライブラリを読み込んで起動する

#10/3の結果を小数点10桁表示
bc
scale=10 
10/3 #小数点10桁
#コマンドを終了
quit
bc -l
10/3 #3.33333333333333333333

# コマンドラインから(文字列(1+0.5)をbcに渡す)
echo "1+0.5" | bc  

シェルスクリプトで
2つの引数の割り算結果を小数点10桁まで表示する
*小数点以下をあつかう

#!/bin/bash
echo "scale=10;$1/$2" | bc

条件分岐

「if文」は与えられた条件式が真のときのみ処理を行います

if 条件式 ; 
then
  処理
fi
if 条件式 ; 
then
  処理1
else
  処理2
fi
if 条件式1 ; 
then
  処理1
elif 条件式2 ; 
then
  処理2
else
  処理3
fi

「testコマンド」は「真か偽か」だけを返すコマンドです
条件式につかいます
メッセージ出力は行いません(echo $?の結果が「0」なら真「1」なら偽になります)

「testコマンド」のオプション

数値を比較
-eq : 等しい(=)
-ne : 等しくない(!=)
a -ge b: a が b 以上(≧)
a -le b : a が b 以下(≦)
a -gt b : a が b より大きい(>)
a -lt b : a が b 未満(<)
文字列の比較
= : 左辺と右辺が等しい
!= :左辺と右辺が等しくない
-n 文字列 : 文字列の長さが0より大きい
-z 文字列 : 文字列の長さが0
例 “abc” = “abc”
ファイルのタイムスタンプを比較
-nt : 左辺が右辺より新い
-ot : 左辺が右辺より古い
ファイルやディレクトリ
-f '名前' : ファイル
-d '名前' : ディレクトリ
-L '名前' : シンボリックリンク
-r '名前' : 読み取り可能ファイル
-w '名前' : 書き込み可能ファイル
-x '名前' : 実行権限があれば
-e '名前' : ファイル(ディレクトリ)が存在する
-s '名前' : サイズが 0 より大きい
論理演算
条件式1 -a 条件式2 : AND(かつ)
条件式1 -o 条件式2 : OR(または)
!条件式 : 条件式が偽であれば真

「testコマンド」は []でもかけます(略式)
*「[ の直後」と「 ] の直前」に必ず半角スペースが必要です
test 1 -eq 1」は「[ 1 -eq 1 ]

「case文」は値とパターンの照合を行い一致したパターンに対応する処理を実行します
上から順に照合して、最初に一致したパターンの処理を行うと終了します
一致しなければ、処理は実行されません

case 値 in
  パターン1 ) 処理1 ;;
  パターン2 ) 処理2 ;;
  …
  パターンn ) 処理n ;;
esac

ループ処理

「for文」は「繰り返し処理の対象(リスト)」が明確なときにつかいます

for 変数 in 値のリスト
do
  変数を処理する
done

#コマンドの実行結果をリストにする
for 変数 in `コマンド`
do
  変数を処理する
done

「while文」は「始めに指定された条件式」が「真」のときにのみループ処理を継続します
*「until文」は「真」になるまで、繰り返します
「break」は、ループを途中で終了
「continue」は、ループをスキップ

# testコマンドで判定
while  [ 条件式 ]
do
  …
done
# ファイルの中身を1行ずつ読み込む
while read 変数名
do
  ...
done <test.txt

*ちなみにwhile :で無限ループに入ります

forやwhileの条件式で(())「二重丸括弧」がつかえます
*(())はC言語スタイルの条件を書くときにつかいます

for (( i=0; i<3; i++ )); do echo $i; done

i=0; while (( $i <= 2 )); do echo $i; ((i++)); done

*例えば、1から100までの数字でfor i in seq 1 100ともかけます

シェルスクリプトで「FizzBuzz」を書いてみる

#!/bin/bash
# while文で
i=1
while (( $i <= 100 ));
do
    if [ $(( i % 3 )) -eq 0 ] && [ $(( i % 5 )) -eq 0 ];
    then
        echo $i FizzBuzz
    elif [ $(( i % 3 )) -eq 0 ];
    then
        echo $i Fizz
    elif [ $(( i % 5 )) -eq 0 ];
    then
        echo $i Buzz
    else
        echo $i
    fi
    i=$(( i + 1 ))
done

# 引数でループ回数をわたし・for文・15の倍数は使わず
for (( i=1; i<=$1; i++ ));
do
    str="";
    if [ $(( i % 3 )) -eq 0 ];
    then
       str="Fizz"
    fi
    if [ $(( i % 5 )) -eq 0 ];
    then
       str+="Buzz"
    fi
    echo "$i:$str"
done

select文

「select文」をつかうと、選択メニューが簡単に実装できます

select 変数 in リスト
do
    選んだ変数の処理
done

メニューとインデックスを表示してループ処理に入ります
インデックスを選択します
ループは「break」で抜けます
if [ループを抜ける条件]; then
break
fi

select command in "list" "delete" "rename" "exit"
do
    case $command in
    "list" )
         ls;;
    "delete")
         ls
         read -p "削除したいファイル名 " file_name
         if [ -f $file_name ];
         then
             rm $file_name
         fi;;
    "rename")
         ls
         read -p "ファイル名" file_name
         read -p "変更したいファイル名 " new_file_name
         if [ -f $file_name ];
         then
             mv $file_name $new_file_name
         fi;;
     "exit")
         break;;
     esac
done

*「select」を使用するときに内部で「$REPLY」という変数がつくられ、選択された値が代入されます

関数をつかう

function 関数名() {
  処理
  return 値
}

「function」は省略可
*関数の呼び出しに「( )」は不要
関数名 引数リスト

「return」について
シェルスクリプトの関数には「戻り値」の概念がなく、標準出力で結果を返します!!

引数の値は、「0 」か「 1~255 」の正整です
「return」が省略されたときは、関数内で最後に実行されたコマンドの終了ステータスになります

戻り値をつかいたいときは、標準出力に戻り値を出力(echo “戻り値”)し、コマンドの結果を文字列として取得します
(下記は、コマンドの実行結果を変数に格納しています)

func() {
  echo "戻り値"
}
# コマンドの実行結果を変数に格納
rtn=`func`

echo "戻り値:${rtn}"

引数について

引数は関数内では 「$1… 」で各引数を参照できます(シェルスクリプトと同様)

ローカル変数について

シェルスクリプトでは関数内の変数はグローバル変数です
ローカル変数としたい変数の前に「 local」 をつけます

「readonly」について
「readonly」指定した変数や関数に対する代入や「unset(削除)」ができなくなります

readonly var      # 変数
readonly -a arr   # 配列は-aオプション
readonly -f func  # 関数は-fオプション

「declareコマンド」は、オプションで変数に属性を付与して、変数を宣言するコマンドです
*declareコマンドを使用すると、オプションがなければローカル変数として定義されます
declare [オプション] [変数名]=[値]

オプション内容
-g関数内でグローバル変数として定義
-l小文字の文字列として定義
-x環境変数として定義
-i整数として定義
-a配列を定義
-A連想配列を定義

シェルスクリプトで「フェボナッチ数」を求めてみる
「0, 1 ,1 ,2, 3, 5 ,8 ,13, 21, 34, 55,89,144 ….(1つ前と2つ前の数を足し合わせた数)」

#!/bin/bash

function fib {
    x=$1
    if [ $x -eq 0 ]; then
        echo 0
    elif [ $x -eq 1 ]; then
        echo 1
    else
        x1=`fib $((x-1))`
        x2=`fib $((x-2))`
        echo $((x1+x2))
    fi  
}

# 引数の数番目のフィボナッチ数を表示する
for (( i=1; i<=$1; i++ ));
do
if [ $1 -eq $i ]; then
      fib "$i"
fi 
done

上の式では、毎回計算する(自分を呼び出す)ので数が大きくなるとすごく時間かかります
計算済数は配列に格納して(メモして)再利用します
*20桁未満w

#!/bin/bash
memo=(
    [0]=0
    [1]=1
    [2]=1
)

for (( i=1; i<=$1; i++ ));
do
if [ $i -gt 2 ]; then
  f1=${memo[$((i-1))]}
  f2=${memo[$((i-2))]}
  res=$((f1+f2))
  memo[$i]=$res
fi
if [ $i -eq $1 ]; then
  echo "$res"
fi
done

デバック

bash -x シェルスクリプト
「-x」オプション : 実行されたコマンドラインが出力されます(変数の値が確認できます)
「+」が付いているコマンドは「シェルスクリプト内」で実行されたコマンド
「++」は 「`` 内」で実行されたコマンド

*「-v 」オプション : 実行するコマンドだけを表示します

部分的なデバック(シェルスクリプトに追記)
実行すると「set -x」 から 「set +x」 までの処理内容が出力されます

# デバッグを開始する
set -x

# デバッグを終了する
set +x

カッコ早見表w

カッコの種類役割
[]条件式 testコマンド等価のコマンド
*「[ の直後」と「 ] の直前」に必ず半角スペースが必要です
(())forやwhileの条件をC言語スタイルで書くとき
$()``と同じで、コマンドの実行結果を文字列として展開
$(())四則演算(計算)を実行して、結果を文字列に展開する
「$(expr ….)」と同じ
*変数をつかうときは、変数の前の$は不要
${}変数を埋め込みとき echo “${hoge}hoge”
配列の表示 echo ${配列名[0]}

余談「/bin/bash」がbashの本体のファイルですが、本来「/binはrootユーザのみが操作できるコマンドを置く場所」なので気になり調べると、「/bin」は「/usr/bin」のシンボリックリンクになっていて「/bin/bash」は実際には「/usr/bin/bash」が起動します(今のLinuxファイル構造的では2つを分ける必要がないそうですw)
ls -l /binを実行すると「/bin -> usr/bin」と表示されます