シェルスクリプトの基本事項まとめ
はじめに
仕事でVBSで作成されたツールをパイプライン上で実行するために、シェルスクリプトに移行する作業を行いました。
シェルはまれに使う機会がありましたが、ガッツリ書いてみると思わぬところでのつまずきが多く、理解が浅いと感じたため学習内容の備忘録を兼ねて投稿します。
本記事は他言語でのプログラミング経験はあるけどシェルスクリプトには触れたことがないという人向けの内容となっています。
(プログラムの基礎的な部分は知っている前提で記載しています。)
VBSについても記載しようかと思いましたが、現在VBSは非推奨となっており、今後使う機会は無さそう(使いたくない)ので、基本的には記載しません。
シェルスクリプトの概要
まずはシェルスクリプトの概要についてChatGPT君に質問してみました。
↓以下回答です。
シェルスクリプトは、UNIXやLinuxオペレーティングシステムのシェルで実行されるスクリプト言語です。
これは、ファイル操作、プログラム実行、テキスト処理などのタスクを自動化するために広く使用されています。
シェルスクリプトの主な特徴は、そのシンプルさと強力なコマンドラインツールとの統合にあります。
システム管理、バッチ処理、日々のルーチンタスクの自動化など、様々な場面でその効率性が活かされます。
ざっくりまとめるとLinux上で使用できるスクリプト言語で、バッチ処理の自動化などに便利なもの。という認識で大丈夫そうです。
シェルスクリプトの基本文法
ここからシェルスクリプトにおけるプログラムの基本処理について記載して行きます。
変数
定義・代入
シェルスクリプトの変数定義・代入は以下のように行います。
変数を定義するときは必ず初期値が必要となります。
#!/bin/bash
x="文字列"
y=10
個人的なつまずきポイントとしては変数名,=,値の間にスペースが入っているとエラーとなってしまう事です。
他言語に慣れているとやってしまいがちだと思うので、気をつけましょう。
参照
シェルスクリプトでは変数の参照時は$変数名または${変数名}とします。
どちらも同じ意味になりますが、${}の方が文字列中に組み込んだりする場合に使い勝手が良いです。
name="hoge"
echo "私の名前は${name}です。"
定数
定数の定義は変数名の前にconstをつけて行います。
const x=10
配列
シェルスクリプトでの配列の定義・使用は以下のように行います。
array={"cal" "p" "f" "c"} # 定義
echo "${array[0]}" # calと出力される
echo "${array[@]}" # 配列の全要素が出力される
array[1]="protein" # array[1] の値がproteinに更新される
i=0
array[$i]="calorie" # 変数を使用したインデックス番号の指定
echo "${array[$i]}" # 変数でインデックス番号を指定しての値の参照
@を使用することで配列の全要素を取得することができ、
!を使用することで間接参照にすることができ、インデックス番号を使用してループ処理を行いたい場合などに便利です。
個人的なつまずきポイントとしてはインデックス番号を指定するときは$を使用して変数を参照する必要がある点です。
以下にインデックス番号の指定に配列と変数を使用した例を記載しておきます。
num_array={1 2 3} # 配列でインデックス番号を指定しての値の参照
echo "${array[${num[$i]}]}" # 配列とインデックス番号を指定しての値の参照
また、シェルでは多次元配列をサポートされていません。
多次元配列を擬似的に再現する方法として連想配列で複数キーを指定する方法があります。
以下に簡単な例を記載しておきます。
declare -A matrix # 連想配列の定義
matrix[0,0]="2次元配列" # 値の代入
x=0
y=0
echo "${matrix[$x,$y]}" # 値の参照
if文
シェルスクリプトではif文を以下のように記載します。
要素モリモリの内容になっています。
if [ $x -eq 0 ]; then # 数値の比較
処理内容
elif [ "$y" = "" ]; then # 文字列の比較
処理内容
elif [ "$y" = "a" ] && [ "$y" = "b" ]; then # and条件
処理内容
elif [ "$y" = "a" ] || [ "$y" = "c" ]; then # or条件
処理内容
else
処理内容
fi
and条件は&&、or条件は||、否定は!を使用して表現します。
使用できる演算子の中でよく使いそうなものを以下に記載しておきます。
演算子
<文字列の比較>
= : 等しい(イコール1つのみ)
!= : 等しくない
<文字列の比較>
= : 文字列が等しい(イコール1つのみ)
!= : 文字列が等しくない
=~ : 文字列に特定の文字列が含まれる
-n : 文字列が空でない
-z : 文字列が空
<数値の比較>
-eq : 数値が等しい
-ne : 数値が等しくない
-lt : <
-gt : >
-le : <=
-ge : >=
<ファイルの判定>
-e ファイルパス:指定したファイル・ディレクトリが存在する
-f ファイルパス:指定したパスがファイル
-d ファイルパス:指定したパスがディレクトリ
-s ファイルパス:指定したファイルのサイズが1バイト以上(空ではない)
また、条件式の前に!をつけることで条件式の成否の判定結果を反転することができます。
個人的なつまづきポイントとしては、
if [ "$x" != "" ]; then を例に
1. []の前後には必ずスペースが必要な点
2. 文字列の比較で文字列が空だった場合、""で囲い文字列化していないとエラーとなる点(文字列比較の場合は常に""で囲っておくのが無難)
3. bool値を持った変数を判定に使用する場合は判別式となる[]が不要になるという点です。
x=true # boolean変数
if x; then # []は不要
case文
シェルスクリプトのcase文は以下のように記載します。
case "$choice" in
"a" )
処理内容
;;
"b" )
処理内容
;;
"c" )
処理内容
;;
* ) # ケースに合致しなかった場合の処理
処理内容
;;
esac
複雑な内容はありませんが;;でケースの内容を修了することを忘れないようにしましょう。
また、シェルスクリプトのcase処理では最初に条件に一致した処理のみ行いcase文を終了します。
複数条件に合致する場合も最初の処理しか行われません。
(個人的にはbrakeを書き忘れて???となることがたまにあるので、
この仕様の方が好みですが、皆さんどう思いますか?
コメントいただけると嬉しいです。)
for文
シェルスクリプトのfor文は以下のように記載します。
回数ループ
for ((i=0; i<10; i++))
do
処理内容
done
c言語をベースとしたループカウンタを使用する一般的な形のfor文です。
配列ループ
array={"a" "b" "c"}
for char in ${array[@]};
do
処理内容
done
配列の要素が一つづつループ変数(今回の場合はchar)に格納されてループが回ります。
一見配列処理に適した使い勝手の良い書き方ですが、この書き方には他言語にはない大きな落とし穴があります。
シェルスクリプトの配列ループは厳密には配列の値を取得しているわけではなく、${array[@]}により配列要素を半角スペース区切りにした文字列が渡され、渡された文字列を半角スペースで区切った要素毎にループが回ります。
つまり、配列要素に半角スペースが含まれる場合は、そこで要素が区切られ、バラバラに処理されてしまいます。
# NG例
array={"a b c" "d" "e f"}
for char in ${array[@]};
do
処理内容
done
上の例だとcharにa b c d e f が順に格納されてそれぞれでループ処理が行われることになります。
(私はここでどハマりしてしまいました。。。)
基本的には回数ループで配列長を指定して、インデックス番号で処理した方が無難かもしれません。
while文
シェルスクリプトでwhile文は以下のように記述します。
while [ 条件式 ]
do
処理内容
done
条件式はif文と同様の内容になります。
コマンドの実行と結果の取得
シェルスクリプトではコマンドを実行し、その結果を取得するには
$()を使用して以下のように記述します。(コマンド置換)
コマンド置換を行うことで、コマンドの実行結果を文字列として取得することができます。
str="磯野ー、筋トレしようぜー!"
result=$(echo "$str")
例文ではechoを使用しているだけですが、|( パイプ)を使用して複数のコマンドを繋げた結果を取得したり、以降の章で記載する少し応用的な内容の処理を行った結果を変数に格納する際などに多用します。
コマンドの実行結果を文字列にせず直接他のコマンドの入力にしたい場合は、プロセス置換を使用します。
個人的なつまずきポイントは以降で説明する算術演算と異なり$()内で変数を使用する場合は${変数名}のように$が必要となる点です。
算術演算
シェルスクリプトでは$(())を使用して以下のように算術演算を行います。
cal=$((p*4+f*9+c*4))
$(())の算術演算内で使用する変数は$をつける必要がありません。
(つけると正しく動作しません。)
個人的なつまずきポイントとしては変数には$をつける必要はありませんが、配列のindex番号の指定に変数や配列要素を使用する場合は$が必要という点です。(私はここもどハマりして大きく時間を取られました。。。)
num_array={1 2 3} # 配列でインデックス番号を指定しての値の参照
num_index={1 2 3}
i=1
num=0
num=$((num + num_array[${num_index[$i]}]))
なんだこのクソコードはと思うかもしれませんが、
クソコードはいつも私たちのそばに。。。
クソコードとの出会いは突然訪れるので覚えておいて損はないです。
私は2次元配列のインデックス番号が2次元配列の値で行われており、さらにそのインデックス番号が配列の値で指定されている神コードと出会いました😇
関数
シェルスクリプトでは関数の定義と呼び出しを以下のように行います。
# 関数の定義
function func1 () {
local x=$1
local y=$2
# 処理
return 0
}
# 関数の呼び出し
func1 "str" 10
function 関数名 () により関数を定義して、
関数名 引数 引数 のように関数を呼び出します。
関数内でローカル変数を使用する場合は、変数名の前にlocalをつけます。
個人的なつまづきポイントは、
1. 関数内処理での引数の取得は$1,$2…で行い()内では行わない
2. 関数の戻り値として設定できるのは0~255のステータスコードのみで、
文字列を直接返却する方法は無い
という点です。
特に文字列を返却できないというのは致命的だと思います。
擬似的に変数の戻り値を返す方法として、あらかじめグローバル変数を作成しておき、戻り値をグローバル変数に格納するという方法があります。
(これが正しい書き方だとは思えないので、正しい方法を知っている方がおられましたら教えていただきたいです。)
少し応用的な内容
ここからはVBSの変換作業など、実作業の中で出会った使えそうな処理について記載して行きます。
正規表現で特定のパターンに挟まれた文字列を取得する
str=$( grep -oP '(?<=正規表現パターン1).*(?=正規表現パターン2)' ) ファイルパス
grepでファイルパスのファイルから正規表現パターン1と正規表現パターン2に挟まれた文字列の配列を取得するコードです。
-o:一致した部分のみ(今回の場合は.*部分)のみを出力するオプション
-P:Perl互換正規表現を使用するためのオプション
(?<=正規表現パターン1):指定した文字列の直後に続く
任意のテキストを捉えるための条件
(?=正規表現パターン2):指定した文字列の直前に続く
任意のテキストを捉えるための条件
数値を文字列に変換
VBSのCstr処理
$( printf "%s" $num)
文字列を区切り文字で区切って配列化
VBSのSplit処理
IFS=',' read -ra 作成する配列名 <<< "配列化する文字列変数"
IFS=','で区切り文字列を,に指定して変数を,ごとに要素分解した配列を作成します。
複数行の入力(ヒアドキュメント)
echoでの複数行を入力しての出力や、
SSHで別環境に複数コマンドを送るなど、
コマンドに対しての入力が複数行になる場合に有用です。
実行したいコマンド <<EOF
line1
line2
line3
EOF
区切り文字列(今回の場合EOF)を入力して次に区切り文字列が現れるまでの内容を一つの文字列としてコマンドに渡すことができます。
区切り文字列は自由に設定できますが、他者が読んだ場合でもわかりやすいという意味でEOFを使うことを推奨します。
ファイル内の特定の文字列を置換(sed)
sedを使用してファイル内の文字列を対象に置換処理を行います。
sed -i 's/"置換対象文字列/"置換後文字列"/g"' test.txt
sにより置換前文字列と置換後文字列を指定する
gにより置換前文字列を全て置換することを指定。
(g無しだと最初の文頭から見て最初の一つのみ置換される)
iオプションでファイルの内容を直接書き換える設定にできる。
iオプションを設定しない場合、結果は標準出力に出力される。
文字列前後の空白(スペース)を削除
VBSのTrim処理
$(echo "対象文字列" | sed -e 's/^ *//' -e 's/ *$//')
sedで文字列の操作を行います。
正規表現を使用したパターン変換により
's/^ *//'で文頭のスペースを's/ *$//'で文末のスペースを削除しています。
-e: 編集コマンド(script)を指定するために使用されます。
複数の編集コマンドを一連の処理として実行する場合に有用です。
変数に格納した文字列の値置換
VBSのReplace処理
${変数//"変換する文字列"/"変換後文字列"}
パラメータ展開を使用して、指定された変数の値の中で特定の文字列を別の文字列に置換しています。
改行コード(CRLF)の削除
変数に格納した文字列の値置換のつまづきポイントになりますが、私は改行コードの置換で大苦戦しました。
${変数//[$'¥r¥n']/}
上記例は変数に改行コードCRLFの文字列を格納したものです。
(通常シェルでは改行コードとしてLFが使用されますが、CRLFを使用されたファイルの内容を変数として保持する場合にCRLFが使用されます。)
¥r¥nを削除すれば良いという考えで文字クラスの[]なしで実行していましたがうまく改行を削除できませんでした。
¥rと¥rの間にスペースなどがあり、うまく反応していないのかと思い、¥rと¥nを別々に削除してみましたが、それでもうまくいきませんでした。
文字クラスが必要な理由をご存知の方がいればコメントいただけると泣いて喜びます。
文字列から一部分を取得
VBSのMid ,Right,Left処理
文字列を指定したフォーマットに従って出力
${"対象文字列":num1:num2}
対象文字列のnum1番目の文字からnum2文字を取得した文字列を作成します。
文字列を全て大文字に置換
VBSのUCase処理
$(echo "対象文字列" | tr '[:lowwer]' '[:upper]')
trコマンドを使用して小文字を大文字に置換しています。
文字列を全て小文字に置換
VBSのLCase処理
$(echo "対象文字列" | tr '[:upper]' '[:lowwer]')
trコマンドを使用して大文字を小文字に置換しています。