シェルスクリプトの基礎 - 入出力

提供:MochiuWiki : SUSE, EC, PCB
2025年1月6日 (月) 23:20時点におけるWiki (トーク | 投稿記録)による版 (→‎ジョブ番号の出力の抑制)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
ナビゲーションに移動 検索に移動

概要

Bashにおける入出力は、スクリプトとデータの間でデータを受け渡しするために重要な役割を果たす。

以下に示す概念を理解することにより、Bashでデータの入力、出力、リダイレクションを適切に行うことができる。
また、パイプを使用してコマンドを連携させることにより、より強力なデータ処理が可能になる。

  • 標準入力 (stdin)
    デフォルトでは、キーボードからの入力を表す。
    readコマンドを使用して、標準入力からデータを読み込むことができる。
    <記号を使用して、ファイルからデータを読み込むこともできる。

  • 標準出力 (stdout)
    デフォルトでは、コンソールへの出力を表す。
    echoコマンドを使用して、標準出力にデータを書き込むことができる。
    また、>記号を使用して、ファイルにデータを書き込むこともできる。

  • 標準エラー出力 (stderr)
    エラーメッセージ等の診断出力を表す。
    デフォルトでは、標準出力と同じ場所 (コンソール) に出力される。
    2>記号を使用して、ファイルにエラー出力を書き込むこともできる。

  • リダイレクション
    <>>>等の記号を使用して、標準入力、標準出力、標準エラー出力の向き先を変更することができる。
    >は指定されたファイルに上書きして、>>は指定されたファイルに追記する。

  • パイプ ( | )
    1つのコマンドの標準出力を別のコマンドの標準入力につなげることができる。
    複数のコマンドをパイプでつなげることで、データを連続的に処理することができる。

  • ヒアドキュメント ( << )
    複数行のテキストを標準入力として渡すことができる。
    <<の後にデリミタを指定して、そのデリミタが再度現れるまでの行が標準入力として扱われる。


入出力の適切な使用は、Bashスクリプトの柔軟性と効率性を高めるために重要である。


入力

readコマンドは、ユーザからの入力を受け取るために使用されるビルトインコマンドである。
readコマンドを使用することにより、スクリプトの実行中に一時停止して、キーボードから入力した内容を変数に格納することができる。

 read <オプション> <変数名>


readコマンドのオプション

オプション 説明
-a <配列変数名> 入力した値を配列変数に格納する。
-n <文字数> 指定された文字数だけ読み込む。
キーボードで直接入力すると、2文字を入力した時点で、自動的に入力処理が終了して値が変数に格納される。
また、コピー&ペーストで2文字以上の文字を入力した場合も、指定した分だけが変数に格納される。
-p <文字列> コンソールに文字列を表示した後、入力待ちになる。 (ユーザに入力を促すメッセージを表示することができる)
改行文字が入力されると、入力の受け付けが終了する。
-r \(バックスラッシュ)を文字列として変数に格納する。
-s 画面上に入力した文字を表示しない。 (サイレントモード)
パスワードの入力等に使用する。
-t <秒数> 入力待ち時間に制限を付ける。 (タイムアウト)


入力データの表示

以下の例では、キーボードで入力したデータを変数DATAへ格納し、その内容を表示させている。

readコマンドで入力データを読み込んだ後は、そのまま処理が継続する。
これを利用することで、何かキー入力されるまで処理を停止させることができる。

 #!/bin/bash
 
 echo -n "Press any key: "
 read DATA
 echo ""
 echo "Entered key: $DATA"


-pオプションを付加することにより、プロンプトメッセージを指定して、ユーザに入力を促すメッセージを表示することができる。

 read -p "表示する文" 変数名


 #!/bin/bash
 
 read -p "Press any key: " DATA
 echo ""
 echo "Entered key: $DATA"


入力データと処理の分岐

caseコマンドと組み合わせることで、入力されたキーの内容によって処理を分岐することができる。

以下では、yキーを入力した場合はOK、nキーを入力した場合はNG、それ以外の場合はPush y or n key.と表示させる。

 #!/bin/bash
 
 read -p "Are you ok ? (y/n): " DATA
 case "$DATA" in
    [yY]) echo "OK" ;;
    [nN]) echo "NG" ;;
    *)    echo "Push y or n key."
 esac


入力データと反復処理

入力した内容によって処理を繰り返したい場合は、whileコマンドと組み合わせる。

以下のシェルスクリプトでは、
Repeat ?(y/n):と表示した後、yキーを入力すると処理を繰り返して、また、Repeat ?(y/n):と表示して入力待ちとなる。
nキーを入力すると、End Repead.と表示してbreakにより繰り返し処理から抜ける。
yキーまたはnキー以外が入力された場合、Input y or n keyと表示して、再度、Repeat ?(y/n):と表示して入力待ちとなる。

 #!/bin/bash
 
 while :
 do
    read -p "Repeat ? (y/n): " DATA
    if [ "$DATA" = "y" ]; then
       echo "Repeat !!"
    elif [ "$DATA" = "n" ]; then
      echo "End repead."
      break
    else
      echo "Input y or n key"
    fi
 done


複数の入力データ

変数を複数用意することで複数の入力を受け付けることができる。

以下のシェルスクリプトでは、変数DATA1、DATA2、DATA3という3つの変数を宣言しているので、
スペース(タブでも可)で区切った3つの値を変数に格納することができる。

 #!/bin/bash
 
 read -p "Press any key: " DATA1 DATA2 DATA3
 echo ""
 echo "Entered key: $DATA1"
 echo "Entered key: $DATA2"
 echo "Entered key: $DATA3"


※区切り文字を変更する場合
区切り文字は、環境変数のIFSを使用することで変更することができる。
以下のシェルスクリプトでは、区切り文字をカンマ(,)に変更して実行している。

 #!/bin/bash
 
 IFS=, read -p "Press any key: " DATA1,DATA2,DATA3
 echo ""
 echo "Entered key: $DATA1"
 echo "Entered key: $DATA2"
 echo "Entered key: $DATA3"


expectコマンド

expectコマンドを使用すると、任意のコマンドの出力を待ち受けて、自動でそれに対する入力を行うことができる。
例えば、SSHやRsyncのパスワード入力を自動化することができる。

expectコマンドがインストールされていない場合は、パッケージ管理システム等を使用してインストールする。

expectコマンドの基本

以下の例では、簡単なユーザ入力を必要とするスクリプトを自動で動作させている。

 #!/bin/sh
 
 read -p "Enter your name: " NAME
 echo "Hello, $NAME"


これを実行すると、以下のようにユーザ入力の待ち受け状態となる。

Enter your name:


この状態において、自動的に入力を行うには、以下のようにexpect -cコマンドを使用する。
以下の例では、外部のシェルスクリプト(Sample.sh)の入力待ちに対して、自動的にMakuという入力を行っている。

 #!/bin/sh
 
 expect -c "
    set timeout 3
    spawn ./Sample.sh
    expect \"Enter your name:\"
    send \"Maku\n\"
    interact
 "
 
 # 出力
 spawn ./Sample.sh
 Enter your name: Maku
 Hello, Maku


expect -cコマンドのダブルクォーテーションで囲まれた部分には、実行するシェルスクリプトを記述する。
また、この内部でダブルクォーテーションを使用する場合は、\"と記述してエスケープしなければいけないことに注意する。

expect -cコマンドの内部の各行は、以下のような意味を持っている。

  • set timeout 3
    外部のシェルスクリプト(Sample.sh)の実行を3秒待つ。
  • spawn ./Sample.sh
    指定した外部プログラム(./greet.sh)を実行する。
  • expect \"Enter your name:\"
    外部プログラムの出力(Enter your name:)を待つ。
  • send \"Maku\n\"
    外部プログラムにMakuと入力して、最後にEnterを入力する。
  • interact
    外部プログラムとのインタラクションへ戻る。


SSH接続やRsync接続のパスワード入力を自動化する

SSHコマンドやRsyncコマンド等のパスワード入力も自動化することができる。
ただし、シェルスクリプト等にパスワードを平文で記述することになるので、ファイルの扱いには十分に注意すること。

以下の例では、SSHコマンドがパスワード入力を求めてきた時、自動でパスワードを入力してログインしている。

 #!/bin/sh
 
 expect -c "
    set timeout 3
    spawn ssh <ユーザ名>@<ドメイン名またはIPアドレス>
    expect \"password:\"
    send \"<パスワード>\n\"
    interact
 "
 
 # 出力
 spawn ssh <ユーザ名>@<ドメイン名またはIPアドレス>
 <ユーザ名>@<ドメイン名またはIPアドレス>'s password:
 Last login: Sat Nov 10 19:58:51 2018 from hogehoge.ap.example.jp


ログイン後に、何らかのコマンド(例 : ls -alF)を実行するところまで自動化するのであれば、
以下のように、プロンプトの$を待ち受けて、次の入力を行えばよい。

 #!/bin/sh
 
 expect -c "
    set timeout 3
    spawn ssh <ユーザ名>@<ドメイン名またはIPアドレス>
    expect \"password:\"
    send \"<パスワード>\n\"
 
    expect \"$\"
    send \"ls -aFl\n\"
 
    interact
 "


また、Rsyncコマンドでファイル転送する場合も、同様にパスワード入力を自動化できる。

 #!/bin/sh
 
 expect -c "
    spawn rsync -r -h -v --delete public <ユーザ名>@<ドメイン名またはIPアドレス>:public
    expect \"Password:\"
    send \"<パスワード>\n\"
    interact
 "



出力

echoコマンド

echoコマンドで変数の値を出力する時は、以下のように、ダブルクォートで囲む必要がある。

これは、変数展開が行われることによりダブルクォートが取り除かれ、echoコマンドに3つのパラメータが渡されたものとして扱われる。 パラメータの区切りとして使用するスペースの数は関係なく、各パラメータ(AAA、BBB、CCC)が1つのスペースで結合されて出力される。

 # 実行 
 $DATA="AAA   BBB   CCC"
 echo "$DATA"
 
 # 出力
 AAA   BBB   CCC


echoコマンドと文字の色

echoコマンドにおいて、出力内容の色を変更するには、以下のように記述する。

 # 実行
 error()
 {
    echo -e "\033[31m$*\033[00m"
 }
 
 warn()
 {
    echo -e "\033[33m$*\033[00m"
 }
 
 info()
 {
    echo -e "\033[32m$*\033[00m"
 }


# 出力
error 'Error message'
warn 'Warning message'
info 'Information message'


標準出力 / 標準エラー出力の抑制

>1>/dev/nullは、標準出力を破棄する。

 ./script.sh 1>/dev/null


>2>/dev/nullは、標準エラー出力を破棄する。

 ./script.sh 2>/dev/null


また、2>&-でも、標準エラー出力を閉じることができる。

 ./script.sh 2>&-


ジョブ番号の出力の抑制

ジョブ番号の出力を非表示にするには、いくつかの方法がある。

  • サブシェルを使用する。
    括弧でコマンドを囲むことにより、サブシェルで実行されてジョブ番号の出力を抑制する。
    または、$()でコマンドを囲むことにより、サブシェルで実行される。
    (aplay $HOME/Login.wav > /dev/null 2>&1 &)
    $(aplay $HOME/Login.wav > /dev/null 2>&1 &)


  • disownコマンドを使用する。
    ジョブがシェルのジョブ制御から切り離され、ジョブ完了メッセージのみが非表示となる。
    aplay ~/Sample.wav > /dev/null 2>&1 & disown


  • シェルのオプションを変更する。
    シェルスクリプトの冒頭に以下を追加することにより、ジョブ制御を無効にできる。
    これにより、バックグラウンドジョブの状態変化の通知が抑制される。
    set +m



リダイレクションとパイプ

コマンドの実行結果は、通常、標準出力であるディスプレイに出力される。

この実行結果はリダイレクション(>、>>)やパイプ(|)を使用することにより、
ディスプレイではなくテキストファイルやコマンドに対して出力するように切り替えることができる。

また、リダイレクションは、コマンドの出力先をテキストファイルに切り替える以外にも、
コマンドへの入力元をテキストファイルに切り替えることもできる。

なお、リダイレクションとパイプは、以下のように使い分ける。

  • リダイレクション
    データを渡す対象がファイルである場合はリダイレクションを使用する。

  • パイプ
    データを渡す対象がコマンドである場合はパイプを使用する。


リダイレクション(>、»、<)

コマンドの実行結果の出力先をディスプレイからファイルへ切り替えたい場合は、リダイレクションを使用する。
コマンドに続けてリダイレクション記号(>、>>)と出力先ファイルを指定することで、コマンドの実行結果をファイルへ書き込むことができる。
ファイルが存在しない場合は、新規にファイルが作成されて出力される。

コマンドの出力をfile.txtへ上書きする。(既存の内容は全て消去される)

command > file.txt


コマンドの出力をfile.txtへ追記する。(既存の内容の最下行に追加される)

command >> file.txt


以下の3種類の応用例では、リダイレクションを使用してファイルの内容を全て削除している。

> file

: >file

cat /dev/null >file


リダイレクションは、コマンドへの入力元をファイルに切り替えることもできる。

# ファイルの内容をコマンドへ渡す
command < file.txt


コマンドに続けてリダイレクション記号(<)と入力元ファイルを指定することで、ファイルに対してコマンドを実行することができる。
また、リダイレクション記号(<)は、コマンドへのリダイレクションではなく、ファイルディスクリプタ0番へのリダイレクションである。

以下の例では、ファイルの行数をwcコマンドでカウントしている。

file.txtファイルの内容
11111
22222
33333


# 実行
wc -l < file.txt

# 出力
5


以下の例では、リダイレクションでファイルを入力元として、ファイルの内容を出力している

 #!/bin/sh
 
 # readコマンドで標準入力から1行ずつ読み込む
 echo "while文にリダイレクション"
 while read line
 do
    echo "$line"
 done < $1
 
 # execコマンドを使用して、カレントシェルの標準入力へリダイレクトする
 echo "カレントシェルにリダイレクション"
 exec < $1
 
 # readコマンドで標準入力から1行ずつ読み込む
 while read line
 do
    echo "$line"
 done
 
 exit 0
 
 # 出力
 while文にリダイレクション
 11111
 22222
 33333
 
 カレントシェルにリダイレクション
 11111
 22222
 33333


パイプ(|)

コマンドの実行結果を他のコマンドへ引き渡したい場合はパイプ(|)を使用する。
複数コマンドをパイプで結合すると、コマンドは左から順に実行され、各コマンドの実行結果はパイプで結合された隣のコマンドへと引き渡される。
実行結果を引き渡されたコマンドは、その引き渡された実行結果に対して処理を行う。

なお、引き渡されるのは標準出力のみで、標準エラー出力は引き渡されない。

command1 | command2
command1 | command2 | … | commandN


# ls コマンドの実行結果を wc コマンドに引き渡して行数をカウントする
ls | wc -l

# 標準エラー出力も渡す場合、パイプの前に2番を1番にリダイレクトする
ls hogehoge 2 > &1 | wc -l


ヒアドキュメント(<<)

コマンドの標準入力に対して、複数行にわたる任意の文字列を与えるには、ヒアドキュメントを使用する。
ヒアドキュメントを使用すると、終了文字が出現するまでの文字列を、コマンドへの標準入力として与えることができる。

ヒアドキュメント中では``$()によるコマンド置換や、変数も使用可能である。
ヒアドキュメント内でのコマンド置換や変数を完全に無効にする場合は、終了文字を'終了文字'というようにシングルクォートで囲むか、
もしくは、\終了文字というようにバックスラッシュでエスケープする。

command << 終了文字


以下の例のように、終了文字は行中にあっても無視される。
ヒアドキュメントを終了するには、終了文字を1行でかつ余計な文字を付けずに記述する必要がある。
余談であるが、以下の例で終了文字として使用されている_EOT_は、End Of Textの略である。前後の_は終了位置を強調するために付加している。

# >記号の部分がヒアドキュメントとしてキーボードから入力した部分である
# その下部は、catコマンドがヒアドキュメントからの入力を出力した部分である
cat <<_EOT_
> hoge hoge
> fuga fuga
> foo foo _EOT_
> _EOT_ bar bar
> _EOT_
hoge hoge
fuga fuga
foo foo _EOT_
_EOT_ bar bar

# ヒアドキュメント中ではコマンド置換や変数も使用可能である
cat <<_EOT_
`echo "hoge"`
$(echo "fuga")
> $PATH
> $HOME
> _EOT_
hoge
fuga
/usr/local/bin:/bin:/usr/bin:/home/username/bin
/home/username


以下の例は、ヒアドキュメントを使用したシェルスクリプトである。

 #!/bin/sh
 
 # catの出力結果を標準エラー出力へ
 if [ $# -ne 1 ]; then
    cat << _EOT_ 1 > &2 
    引数を指定してください。
    Usage: $0 param
    _EOT_
    exit 1
 fi
 
 # $を表示する場合、\$のようにエスケープする
 cat << _EOT_
    ヒアドキュメント中では変数も使用できます。
    \$1 は $1 です。
    _EOT_
 
 # 終了文字をエスケープする場合、ヒアドキュメント中の変数は展開されない
 cat << '_EOT_'
    シングルクオートで終了文字を囲むと変数は無視されます。
    \$1 は $1 です。
    `echo "コマンド置換も無視されます。"`
    _EOT_
 
 cat << \_EOT_
    バックスラッシュでも同様です。
    \$1 は $1 です。
    `echo "コマンド置換も無視されます。"`
    _EOT_
 
 # <<-とすると、ヒアドキュメント中の先頭にあるタブは無視される(スペースは無視されない)
 cat <<-_EOT_
    終了文字の前に-を指定する場合、ヒアドキュメント中の先頭のタブは無視されます。
    ←タブ
    ←タブ
    _EOT_
 
 exit 0
 
 # 出力
 ヒアドキュメント中では変数も使用できます。
 $1 は HEREDOC です。
 
 シングルクオートで終了文字を囲むと変数は無視されます。
 \$1$1 です。
 `echo "コマンド置換も無視されます。"`
 
 バックスラッシュでも同様です。
 \$1$1 です。
 `echo "コマンド置換も無視されます。"`
 
 終了文字の前に-を指定する場合、ヒアドキュメント中の先頭のタブは無視されます。
 ←タブ
 ←タブ


複数行の文字列を出力する場合、このヒアドキュメントを使用することを推奨する。
echoやprintfで複数行出力する場合は'や"で囲む必要があるため、出力する文字列にそれら自体が含まれているとエスケープする必要があるが、
ヒアドキュメントであれば、それらのエスケープを意識する必要はない。

以下に、ヒアドキュメントの使用方法をまとめる。

  • << _EOT_
    通常のヒアドキュメント。
    ヒアドキュメント内で変数が使用可能。($を表示するには、\$のようにエスケープする)
    コマンド置換が使用可能。

  • << '_EOT_' または << \_EOT_
    変数展開およびコマンド置換を実行しないヒアドキュメント。
    ヒアドキュメント内で変数は使用できない。($がそのまま出力される)
    コマンド置換が使用不可。(``および$()がそのまま出力される)

  • << -_EOT_
    行の先頭にあるタブを無視する通常のヒアドキュメント。
    ヒアドキュメント内で変数を使用可能。($を表示するには、\$のようにエスケープする)
    コマンド置換が使用可能。
    ヒアドキュメント内の行の先頭にあるタブが無視される。

  • << -'_EOT_' または << -\_EOT_
    行の先頭にあるタブを無視して、変数展開およびコマンド置換を実行しないヒアドキュメント。
    ヒアドキュメント内で変数は使用できない。($がそのまま出力される)
    コマンド置換が使用不可。(``および$()がそのまま出力される)
    ヒアドキュメント内の行の先頭にあるタブが無視される。


プロセス置換 <()

プロセス置換とは、コマンドの出力結果をファイルとして扱う機能である。
例えば、diffコマンドは、引数にファイル以外を指定することができないが、
プロセス置換を使用することで、コマンドの実行結果をdiffコマンドに直接渡すことができる。

実行結果をファイルとして使用したいコマンドを、< ()で指定する
diff < (command) < (commandA; commandB)


以下の例のように、プロセス置換は、一時ファイルを作成せずにファイルの置換結果を確認したい場合等に有効である。

# text.txtファイル(置換対象のファイル内容)
hoge
fuga
foo
bar


# 実行
diff text.txt < (sed -e 's/hoge/HOGE HOGE/' text.txt)

# 出力
1c1
< hoge
---
> HOGE HOGE


プロセス置換に使用するコマンドは、単一のコマンドのみではなく、セミコロン区切りの複数コマンド、パイプやネストしたプロセス置換も指定可能である。

以下の例では、diffコマンドを使用して、2つの入力ストリームの差分を表示している。

  1. echo "aaa"; echo "bbb"
    まず、aaaとbbbという2行からなる入力ストリームを生成する。
  2. < (echo "aaa"; echo "bbb")
    上記の2行の入力ストリームになる。
    <はdiffコマンドへのリダイレクトを意味しており、この入力ストリームをdiffの最初の入力として使用する。
  3. cat <(echo "aaa") | cat <(echo "BBB")
    <()はプロセス置換を意味する。
    つまり、括弧内のコマンドの出力をファイルのように扱うことができる。
    echo "aaa"の出力をcatに渡して、さらに、echo "BBB"の出力をcatに渡して、2つの出力を連結している。
    結果として、aaaとBBBという2行のストリームができる。
  4. echo "ccc"
    cccという1行のテキストを生成する。
  5. < (cat <(echo "aaa") | cat <(echo "BBB"); echo "ccc")
    この括弧内では、上記3.と4.を組み合わせて、aaa、BBB、cccの3行からなるストリームを生成している。
    そして、<でこのストリームをdiffコマンドの2番目の入力としてリダイレクトしている。


したがって、このコマンド全体では、aaaとbbbの2行からなるストリームと、aaa、BBB、cccの3行からなるストリームの差分をdiffコマンドで表示している。

# 実行
diff < (echo "aaa"; echo "bbb") < (cat <(echo "aaa") | cat <(echo "BBB"); echo "ccc")

# 出力
1,2c1,2
< aaa
< bbb
---
> BBB
> ccc