Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

ちょっとしたシェルスクリプト(bash)の書き方

シェルスクリプト(bash)の書き方

別に本格的なコマンドを作ろうってわけじゃないけど、忘れた頃にちょこっと使うんですよねー。
毎回調べてやってるのでまとめます。

オプション解析とかはしないです。
-eとか-hとか-f /path/to/fileとかが必要になったらここに書いてることを応用して
ゴリゴリループで回して解析します。

$

\$ 意味 具体例
$# 指定された引数の数 コマンド 引数1 引数2だと 2 になる
$@ 指定された引数全てを表す コマンド 引数1 引数2だと引数1 引数2という文字列になる。$*とほぼ同じだが""の中だと挙動が変わる
$* 指定された引数全てを表す コマンド 引数1 引数2だと引数1 引数2という文字列になる。$@とほぼ同じだが""の中だと挙動が変わる
$? 直後のコマンドの戻り値 test 1 -eq 1をした後(真)だと 0 になる。
$0 実行時のコマンド名
$1, $2, ..., $n $1は 1 番目の引数、$2は 2 番目、$nは n 番目 $10以上は$1"0"と区別がつかないので${10}と書く。

使用例

c 言語の argc と argv みたいなものを定義

#!/bin/bash

# 引数の数 + 1(コマンドの分)
argc=`expr $# + 1`

# 引数の配列
argv=($0 $@)

入出力

標準入力の文字列を変数に入れる

read str

標準出力に文字列を出力

echo "output!" >&1

標準エラー出力に文字列を出力

echo "error" >&2

ファイルディスクリプタ

fd 出力先
0 標準入力
1 標準出力
2 標準エラー出力
任意の入出力

テスト用スクリプト

標準入力で受け取った文字列を標準出力する。
標準エラー出力に"error"を出力。

スクリプトファイル名: inout

#!/bin/bash

echo "please input: " # 出力先を指定しない場合echoは標準出力
read str # 標準入力(キーボードからの入力待ち)
echo "$str" >&1 # 標準出力
echo "error" >&2 # 標準エラー出力
# 結果
# please input:
# aiueo <= 標準入力
# aiueo <= 標準出力
# error <= 標準エラー出力

このスクリプトを使用してリダイレクトテストする

リダイレクトについて

リダイレクトはコマンド 1> ファイル名のようにすることでコマンドの標準出力をファイルに書き込むこと
1>はファイルを丸ごとコマンドの標準出力に書き換える。
1>>はファイル末尾にコマンドの標準出力を追記する。

エラー出力をファイルに書き込みたい場合は12に書き換えれば良い

標準出力のみを書き込みたい場合は1>1を省略してコマンド > ファイル名と書ける

標準出力とエラー出力を同じファイルに書き込みたい場合は1> ファイル名 2>>ファイル名とする。
標準出力とエラー出力を別々のファイルに書き込みたい場合は1> ファイル1 2> ファイル2とする。

表にまとめる

リダイレクト 意味
コマンド 1> ファイル 標準出力をファイルに出力
コマンド 1>> ファイル 標準出力をファイルに追記
コマンド 2> ファイル 標準エラー出力をファイルに出力
コマンド 2>> ファイル 標準エラー出力をファイルに追記
コマンド > ファイル 1>の省略記法
コマンド >> ファイル 1>>の省略記法
コマンド &> ファイル 標準出力と標準エラー出力をファイルに出力
コマンド &>> ファイル 標準出力と標準エラー出力をファイルに追記
コマンド 1> /dev/null 標準出力を破棄する。/dev/nullはゴミ箱みたいな
コマンド 2> /dev/null 標準エラー出力を破棄する
コマンド &> /dev/null 標準出力と標準エラー出力を破棄する
コマンド 1> ファイル1 2>ファイル2 標準出力と標準エラー出力を別々のファイルに出力
コマンド 1> ファイル 2>/dev/null 標準出力をファイルに出力し標準エラー出力を破棄する
コマンド 1> /dev/null 2>ファイル 標準出力を破棄し標準エラー出力をファイルに出力
コマンド 1> ファイル 2>>ファイル 標準出力をファイルに出力した後、同じファイルに標準エラー出力を追記
コマンド 2> ファイル 1>>ファイル 標準エラー出力をファイルに出力した後、同じファイルに標準出力を追記

細かいけど大事なことを言うと

コマンド 1> ファイルは、ファイルを書き込みで開いてファイルディスクリプタを 1 とすることを意味する。
echo "output!" >&1において&1の通常の出力先は標準出力だが、今&1は指定されたファイルになっているので
結果的に「標準入力をファイルに出力する」ことになる。

これの意味するところのテスト

コマンド 3> ファイル 1>&3
3>ファイルはファイルを書き込みで開いてファイルディスクリプタ3でアクセスできるようにしている。
1>&3は標準出力を 3 に書き込むことにしている。
結果的に「標準出力をファイルに出力する」ことになる

わかりづらいので色んな説明文を書いて反復します。
言いたいことは全部同じです。

  1. 1 標準出力
  2. 3>ファイル 3 はファイル出力
  3. 1>&3 1 は 3
  4. 1=標準出力、3=ファイル出力、1=3 => 標準出力=ファイル出力
  5. 結果1>ファイルと一緒

色々イコールで結んじゃったけど語弊がありそうね。

どんな需要があるのかわからないが、標準入力と標準出力を入れ替えることもできる。
コマンド 3>&1 1>&2 2>&3
スクリプト内で&1に出力しているものは1>&2でエラー出力となる
スクリプト内で&2に出力しているものは2>&3で 3 に出力、3>&1なので標準出力となる。
結果&1がエラー出力、&2が標準出力になっているので入れ替わったことになる。

リダイレクトテストの具体例をつらつらと。

標準出力を stdout.txt に書き込む

$ inout 1> stdout.txt
aiueo <= 標準入力
error <= 標準エラー出力

stdout.txt

please input:
aiueo

標準エラー出力を error.log に書き込む

$ inout &> error.log
please input:
aiueo <= 標準入力
aiueo <= 標準出力

error.log

error

標準出力と標準エラー出力を out.txt に書き込む

$ inout &> out.txt
aiueo <= 標準入力

out.txt

please input:
aiueo
error

標準出力と標準エラー出力の順番を変えて out.txt に書き込む

$ inout 2> out.txt 1>> out.txt
aiueo <= 標準入力

out.txt

error
please input:
aiueo

標準出力と標準エラー出力を破棄する

$ inout &> /dev/null
aiueo <= 標準入力

標準入力の受け取り方

$ echo "aiueo" | コマンド

みたいな感じで標準入力を受け取りたい

stdin=`cat`
echo "$stdin"

catで標準入力を受け取れる。

こいつを 1 行ずつ処理する

stdin=`cat`

echo -e "$stdin\n" | while read line
do
  echo "$line"
done

read は改行で行を判定する。
標準入力の最終行に改行がない場合があるので\nを入れておく。

if の条件式

test コマンドを使用する。
if test $a -eq $b; then
書きづらいので test の略式記法の[]を使用する。
書き換えると
if [ $a -eq $b ]; then
この時[]の内側に 1 つスペースを入れなければならない。
if [$a -eq $b]; thenはエラーになる。

オプション 意味
-eq [ $a -eq $b ] 数値比較 a == b
-ne [ $a -ne $b ] a != b
-lt [ $a -lt $b ] a < b
-le [ $a -le $b ] a <= b
-gt [ $a -gt $b ] a > b
-ge [ $a -ge $b ] a >= b
-z [ -z $str ] str の文字列長が 0
-n [ -n $str ] str の文字列長が 0 より大きい
-e [ -e $file ] file が存在している
-d [ -d $file ] file がディレクトリ
-d [ -f $file ] file がファイル
-r [ -r $file ] file が読み取り可能
-w [ -w $file ] file が書き込み可能
-x [ -x $file ] file が実行可能
-a [ -n $file -a -e $file ] AND 演算子 ファイル名が空でなく、ファイルが存在する
-o [ -z $file -o -d $file ] OR 演算子 ファイル名が空あるいは、ファイルがディレクトリ
! [ ! -e $file ] NOT 演算子 ファイルが存在しない
== [ "$str" == "aiueo" ] 文字列比較 同一で true
!= [ "$str" != "aiueo" ] 文字列比較 同一で false

使用例

コマンドの引数の数を判定する。

if [ $# -ne 2 ]; then
  echo "引数の数は2つ指定してください"
  exit 1
fi

[]と[[]]

[[]]内ではワード分割されない

[]では

str="aaa bbb"
if [ $str == "aaa bbb" ]; then

のように$strをダブルクォーテーションで囲わないと

[ aaa bbb == "aaa bbb"]

に展開されエラーとなる。
ダブルクォーテーションで囲うと

[ "$str" == "aaa bbb" ]

はダブルクォーテーションの中で変数展開されるので

[ "aaa bbb" == "aaa bbb"]

となりエラーでなくなる。

これを[[]]で書くと

[[ $str == "aaa bbb" ]]

ワード分割されないので

[[ "aaa bbb" == "aaa bbb" ]]

となる。

そのほかにも色々異なることがたくさんある。
[[]]の中では AND や OR がそれぞれ&& || のように書かなければいけない。-a,-oはエラー

そこまで気を使わず普通に test コマンド([])を使用して、文字列変数はダブルクォーテーションで囲うのが吉かな。

カレントディレクトリ

実行スクリプトのあるディレクトリに移動する
~/dev/sh/hogeというスクリプトがあり~/dev/shにパスも通しているとする。

~/dev/sh/hoge

#!/bin/bash
cd `dirname $0`
pwd

~/で実行すると

~ $ hoge
/Users/ユーザー名/dev/sh

となる。

ちなみに実行されたディレクトリは上記の cd を使う前にpwdすれば良い。

scriptPath="$0"
currentDir=`pwd`
cd `dirname "$scriptPath"`
scriptDir=`pwd`

echo "currentDir = $currentDir"
echo "scriptDir = $scriptDir"

# 結果
# currentDir = /Users/ユーザー名
# scriptDir = /Users/ユーザー名/dev/sh

全てのシェルスクリプトの冒頭にテンプレで
これら書くことを心に決めた。

for

配列 arr の走査

arr=(1 2 3 4 5)
for e in ${arr[@]}
do
  echo $e
done
# 結果
# 1
# 2
# 3
# 4
# 5

値リストの走査

for e in 1 2 3 4 5
do
  echo $e
done
# 結果
# 1
# 2
# 3
# 4
# 5

in の右側を""でくくる

for e in "1 2 3 4 5"
do
  echo $e
done
# 結果
# 1 2 3 4 5

in の右側で一部""でくくる

""でくくると 1 つのかたまりとして扱われる

for e in "1 2" "3 4 5"
do
  echo $e
done
# 結果
# 1 2
# 3 4 5

in の右側にコマンド

ディレクトリ以下のファイルに対して色々できる

for e in `ls`
do
  echo $e
done
# 結果
# ファイル1
# ファイル2
# ファイル3
# ファイル4
# ファイル5

seq で値 1 から値 2 までループする

seq 1 10だと 1〜10(カウントアップ)
seq 10 -1だと 10~-1(カウントダウン)

for i in `seq 値1 値2`
do
  echo $i
done

while

while 条件式
do
  処理
done

for と while 共通でループ内では
break : ループから抜ける
continue: 次のループへ

ファイル処理

a.txt

aaa
bbb
ccc
ddd
eee

a.txt の内容を変数に入れる

atxt=`cat a.txt`
echo "$atxt"
# 結果
# aaa
# bbb
# ccc
# ddd
# eee

a.txt の内容を for で回す

for line in `cat a.txt`
do
  echo "> $line"
done
# 結果
# > aaa
# > bbb
# > ccc
# > ddd
# > eee

a.txt の内容を while で回す

cat "a.txt" | while read line; do
  echo "> $line"
done
# 結果
# > aaa
# > bbb
# > ccc
# > ddd

最終行の eee が表示されていない。
read は改行を期待するが eee の後には改行がないのでその行は処理されない。
while の条件式に「$lineが空文字でない」時も回るようにする

cat "a.txt" | while read line || [ -n "$line" ]; do
  echo "> $line"
done
# 結果
# > aaa
# > bbb
# > ccc
# > ddd
# > eee

注意
| while readでファイル内容を回す場合にはこの while はサブプロセスとして動くらしい。
そのため、ループを抜ける場合はbreakでなくexitでサブプロセスを終了する必要があるとか。

演算

整数の演算

exprコマンドが有名のご様子

$ expr 1 + 2
3

bash では(())の中に数式を書いたら計算してくれるとか。
その値を取り出すのは$((数式))のように$をつける。

$ echo $(( 5 + 2 ))
7

こっちの方が速いらしい。

他のシェルだと動かないかもしれんらしいから気をつけよう。
自分用シェルスクリプトで bash しか使わんから私はこちらを使用します。

実数の演算

exprとか(())は整数しか扱えない。
もし割り算で割り切れないのが出てきたら小数点以下は切り捨てられる。

$ expr 10 / 3
3

実数の演算がしたかったらbcコマンドを使う。

$ echo "scale=5; 10.0 / 3.0" | bc
3.33333

scaleで小数点以下の桁数を指定する。

bcは標準入力から数式を受け取るのでechoからパイプで渡す。

この辺の演算なら c で書いた方が楽よね。

あとはコマンドの語彙力勝負

awkとか便利らしい。
sedが苦手だけどこれ使えたらシェルスクリプトがもっと簡単にできそう。
trもよく目にするけど使ったことない。

わざわざスクリプトを全力で書かんでもbashrcの関数とか alias でできるならそっち使おう。

あとはやりたいことググって素敵なコマンドに出会ってくれ

シェルスクリプトのテンプレを生成するやつ

パスの通ったディレクトリにmkshというスクリプトを作った。
新しいスクリプト作るときはそいつを使うことにした。

だって覚えられないしテンプレファイルを複製して作ればいいけど
往往にしてテンプレファイルを編集してしまったりするので。

mksh
第一引数にスクリプト名、第二引数はオプショナルで受け取りたい引数の数。
これでとりあえずテンプレートが吐き出される。
mksh と同ディレクトリにスクリプトが作られる。

そもそもこのスクリプトを見ればだいたいやりたいことやれる。

#!/bin/bash
argc=`echo $(( $# + 1 ))`
argv=($0 $@)

# 今のディレクトリを取得
currentDir=`pwd`

# このスクリプトのディレクトリに移動
cd `dirname ${argv[0]}`
scriptDir=`pwd`
if [ $argc -lt 2 ]; then
  echo "usage: mksh script_name [arguments length]"
  exit 1
fi

# スクリプト名
shname="${argv[1]}"

# ファイルの存在チェック
if [ -e `pwd`"/$shname" ]; then
  echo "`pwd`/$shname is already exists."
  exit 1
fi

# ファイル作成
touch "$shname"
chmod +x "$shname"

# 中身を書く
echo "#!/bin/bash" >> "$shname"
echo 'argc=`echo $(( $# + 1 ))`' >> "$shname"
echo 'argv=($0 $@)' >> "$shname"
echo 'currentDir=`pwd`' >> "$shname"
echo 'cd `dirname $0`' >> "$shname"
echo 'scriptDir=`pwd`' >> "$shname"

# スクリプトの引数指定があれば
if [ $argc -gt 2 ]; then
  # 数値判定
  (( ${argv[2]} + 1 )) &> /dev/null
  if [ $? -eq 0 ]; then
    shargc=${argv[2]}
    echo 'if [ $argc -ne '$shargc']; then' >> "$shname"
    echo '  echo "usage: '"$shname"' arg1"' >> "$shname"
    echo '  exit 1' >> "$shname"
    echo 'fi' >> "$shname"
  fi
fi
# 編集に入る
vim `pwd`"/$shname"

おそらくたくさん間違ったこと書いてるけど
自分用で動いてるから良しとした。

追記 20190812

@angel_p_57 さんご指摘ありがとうございます。

標準出力とエラー出力を同じファイルに書き込みたい場合は1> ファイル名 2>>ファイル名とする。

それはUNIX系一般の話として、書き込みの競合 ( 内容が混ざるという意味ではない ) を起こす可能性があるので、お勧めできません。1> ファイル名 2>&1 です。

…おそらく、標準入出力・リダイレクト自体に重大な誤解があるように思います。
「標準出力」とは、なにかのファイルであったり出力内容を指す用語ではありません。標準で出力に使われる「経路」のようなものであり、ファイルディスクリプタ1番そのものです。
なので、どんなリダイレクトを行おうとも1番が標準出力であることには変わりません。ただ接続されるファイルが変わるだけです。

バチボコ勘違いしてました。
標準出力とか標準エラー出力は「経路」みたいなものと聞いて気持ちよくなりました。
1つのファイルに2つの経路から同時に出力していくのはそりゃダメだよなー。
Cで同一ファイルopenしちゃダメだよーって昔言われたのと同じことっちゃ同じことよね。

コマンド 1> ファイル 2>&1なら

  1. 1の経路はファイルに繋げる。
  2. 2の経路に流す予定だったものを1に流す。(2の経路を1に繋げる?)
  3. ファイルにつながる経路は1つだから気持ち悪くない!

ありがとうございました!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
4
Help us understand the problem. What are the problem?