1288
1310

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Bashの便利な構文だがよく忘れてしまうものの備忘録

Last updated at Posted at 2015-11-26

Bashでスクリプトを書く場合のポイントをまとめた

Bashなどで書くシェルスクリプトは文法がシンプルで覚えると便利。他のプログラム言語だと何行もかかなければならないファイル操作や中身のデータ処理を、ちょちょいと数行レベルで書けとても強力だ。コマンドとの親和性が高い恩恵だろう1

旧来バッチ処理で本領を発揮するシェルスクリプトだったが、昨今はDocker(Dockerfileや起動スクリプトなど)で活用される機会も多いだろう。
しかしながら他のプログラム言語ではみられない記法があったり、逆に似た書き方もあったりと、混乱しがちなのもシェルスクリプトの特徴かもしれない。

いちいちGoogle先生に確認するのもめんどうなので、このページに整理した2

主な参照先(よくお世話になっているページ):

記法・特殊変数

基本

  • そんな高級な言語仕様じゃないし、わりとデバッグやテストもしにくい。
    • でも他のプログラム言語だとコードを何行も書かなければならない処理も、コマンド数発とパイプのワンライナーでいけたりとか、その爽快感は魅力。
  • 一部の言語にあるような命令後の;(セミコロン)は要らない。ただし1行に複数命令を各場合は、明示的に;で区切る。
  • コメントは#を用いる。#以降が無視される。
  • if ~ fi とか case ~ esac とか、他の言語じゃ見られないような洒落た文法。
  • 括弧はいろいろな文法を表現できるようになっている。別記事にまとめてみたので、ご参照あれ。 - Bashの括弧のノウハウ(まとめ)

シェバング

スクリプト行頭の#!で始まる行をシェバング(シェバン、シバンとも)といい4、なんのプログラムで実行すべきかを指し示すことができる。シェルスクリプトの場合は、次のような形になるだろう。

#!/bin/sh
#!/bin/bash

スクリプトファイルに実行パーミッションを設定して直接実行すると、シェルはこの1行目を見て該当するコマンドを用いてスクリプトファイルを実行する。

ほとんどのLinuxのshbashへのリンクとなっている5が、シェバングがshで実行されようとしている場合には、Bashはできるだけshの動作をしようとする。その結果Bash独自の記法が一部使えなくなるので注意だ。(こちらの解説が参考になる。)

特にこだわりがなければ、#!/bin/bash とするのが無難で良いだろう。

shスクリプト
#!/bin/sh
grep 'fuga' < <(cat hoge.txt)    # エラー。sh は <() を解釈できない。
bashスクリプト
#!/bin/bash
grep 'fuga' < <(cat hoge.txt)    # OK. Bashの構文として解釈される。

変数・文字列

変数と代入

任意の文字列を使った変数が作れる6。参照時には$をつけるが、代入する際は$を使わない。代入は=を用いる。
右辺式が文字列の時で、(途中に空白などがなく)式として成り立っていればクォーテーションしなくても代入が可能。
=の前後に可読性目的で空白を入れてはいけない。言語的にエラーである。
他のスクリプト言語のように${VALNAME}といった形で{}で囲むことでき、明示的に文字列と区別させることが可能。

GREETING='Hello! World!!'
MYNAME=HAL3
NOW=`date +'%Y/%m/%d %H:%M:%S'`
echo "${MYNAME}_$NOW> $GREETING"
# HAL3_2018/01/12 12:34:56> Hello! World!!

文字列連結とクォーテーションマーク

文字列連結

連結するには演算子などは必要なく、例のように繋げて記述すれば良い。繋げる文字列の間での空白は不要。

$ HOGE="hoge"
$ FUGA="fuga"
$ echo $HOGE$FUGA      # hogefuga
$ echo "hoge""fuga"    # hogefuga

ダブルクォーテーションマーク

ダブルクォーテーションマークで囲んだ文字列では、式が展開される7

$ NUM=42
$ echo "THE ANSWER IS $NUM"           # THE ANSWER IS 42
$ echo "my HOSTNAME is `hostname`"    # my HOSTNAME is xxx

シングルクォーテーションマーク

シングルクォーテーションマークで囲んだ文字列では、式は展開されない7
!!!などはBashはヒストリを参照する時の特殊コマンドになるので、これを文字列に埋め込むにはシングルクォーテーションで囲むなどしてエスケープする必要がある。

$ echo 'THE ANSWER IS $NUM'          # THE ANSWER IS $NUM (展開されない)
$ echo 'Hello!!'                     # ""だと、 !! が直前のコマンドに置き換わる

文字列の中へのクォーテーションマークの展開

クォーテーションマークの中で、別なクォーテーションマークをエスケープなしで記述が可能である。

$ BOO='TSUNODA "STAR" HIRO'
$ echo $BOO                          # TSUNODA "STAR" HIRO
$ POO="NISHIKINO 'STAR' AKIRA"
$ echo $POO                          # NISHIKINO 'STAR' AKIRA      
$ STAR="☆"
$ perl -e 'print qq(LUCKY '"'"$STAR"'"' STAR\n)'  # LUCKY '☆' STAR

位置パラメータ

スクリプト実行時に引数として渡された内容は、位置パラメータと呼ばれる特殊変数に保持される。これらはスクリプト中では、$1$9で表わせる。
10番目以降の位置パラメータは、${10}のように記述する。
なお、位置パラメータは関数への引数を展開する場合にも使う。

$ piyopiyo.sh "hoge" "fuga" 3 4 5 6 7 8 9 ten eleven
# $1    => hoge
# ${10} => ten

$*, $@, "$@" の違い

位置パラメータを集合的に扱う場合は、$*, $@, "$@"を利用する。
クォーテートしない$@$* と同じだ。なので実質$*"$@"を使い分ければ良い。

sample.sh
echo '--- $* ---'; for P in $*; do echo $P; done
echo '--- $@ ---'; for P in $@; do echo $P; done        # $* と同じ
echo '--- "$@" ---'; for P in "$@"; do echo $P; done
$ ./sample.sh "1 2" "hoge fuga"
--- $* ---
1
2
hoge
fuga
--- $@ ---
1
2
hoge
fuga
--- "$@" ---
1 2
hoge fuga

Bashの変数展開時の置換

Bashの便利機能。たくさんある。こちらのQiitaページに詳しく書かれている

bash
echo $HOGE            # (null)
echo ${HOGE:-hoge}    # hoge    $HOGEがNULLの場合に"hoge"に置き換える。代入はされない。
echo $HOGE            # (null)

echo $FUGA            # (null)
echo ${FUGA:=fuga}    # fuga    $FUGAがNULLの場合に"fuga"に置き換え、かつ、代入もする。
echo $FUGA            # fuga
bash
# change suffix of files
for F in *.JPG;do mv $F ${F//.JPG/.jpg};done;
# cut matched string at head of value
while read URL;do echo ${URL#http://};done < urls.txt
# cut matched string at foot of value
for F in *.tmp;do echo ${F%.tmp};done
# cut matched string as long as pattern can from head
myfilename=${0##*/}    # same as basename

配列

Bashでは配列が使える。
使い方については下記ページの解説が詳しいので、ここでは割愛する。(個人的にあまり使ったことがないというのもあるので…)
Qiita - bash 配列まとめ

コマンド実行

任意のコマンドは、スクリプトファイルで記述して実行したり、CLI上で実行が可能。
スクリプトファイル内でもパイプやリダイレクトなど、もちろん使える。

#!/bin/bash

TODAY=`date +'%Y%m%d'`
cat ./result.txt | grep "^$TODAY"
echo "done" `date +'%H:%M:%S'` > ./debug.log

組み込みコマンド

echoprintfのように、同じ名前で実行ファイルで存在しているコマンド(ex:/bin/echo、”外部コマンド”とも)と、シェルの組み込みとして存在しているコマンドがあったりする。これを”組み込みコマンド”という。
シェルスクリプト上ではパスを指定しないと組み込みのものが使われる。組み込みを使うことで困ることはないだろうが、両者に機能に若干の差異があったりする場合もあるので、実行ファイルのほうを使う場合は明示的にパスを指定するなどして区別する。8

echo.sh
#!/bin/sh

echo -e '\u611B'        # 組み込みコマンドのほう
/bin/echo -e '\u611B'
実行結果
$ echo.sh
愛
\u611B

コマンド実行結果の終了コード

シェル上やスクリプト実行時、直前のコマンドの終了コードは特殊変数$?に代入される。終了コードは0(ゼロ)の場合は正常終了を意味し、それ以外はエラーやそれに準ずる状態である9

$ date
Wed Dec 28 14:37:26 JST 2016
$ echo $?
0
$ hogehoge                             # 存在しないコマンド
-bash: hogehoge: command not found
$ echo $?
127                                    # bash がセットしたエラーコード

なお、ifwhileも、0を真値、それ以外を偽値とした真偽判断を行なっている(後述testの項を参照)

バッククォートと$( )

バッククォート10を使って、`command`でコマンドの実行結果を変数に格納できる。これは伝統的な構文で、最近では $(command) も使える。$()は入れ子にできる。

#!/bin/sh

TODAY=`date +'%Y/%m/%d'`
echo $TODAY
NOWTIME=$(date +'%H:%M:%S')
echo $NOWTIME

NORM300=$(expr $(date +%s) / 300 \* 300)
echo $NORM300

外部ファイルからのコマンド実行 source

source は組み込みコマンドで、外部ファイルを読み込み、中に記述されているコマンドを現在のプロセスで実行する。読み込む外部ファイル自体は実行可能権限が設定されていなくても大丈夫。
.source コマンドの別名として使える。.bash_profile でも使われているので確認してみよう。

~/.bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

何もしないNOP

NOP とは No OPeration のことで、何もしないという命令。Bashでは :(コロン)でそのようなことができ、これは何もせずに終了コード0(正常終了)を返す。
使う場面は、可読性目的でcase文中においたり、無現ループなど。
また、引数を取れるので、本来のコマンドと一時的に置き換えたりして、デバッグや保守目的(暫定対応)などに使える。

if [ "x$wanna" = "xsleep" ]; then
    : sleep 60    # for debug.
fi

判断式

test

真偽を判断するのにコマンドtestを用いる。
シェルでは伝統的に、0が正、0以外が偽である。
またifなどと使われる[も実はコマンドで、testと同じ機能を提供する([ 式 ] という形で用いる)。コマンドtestについての説明は、「UNIXの部屋」のtestの解説が分かりやすい。 11

演算子

数値比較や文字列比較、論理式などの他に、ファイルの状態をチェックできたりが可能。
他の言語にあるような不等号記号は使えないので若干クセがあるが、慣れてしまえば問題ないかと思われる。

演算子やファイルテスト演算子の説明に関しては、こちらのページでまとめてくれている。

logic.sh
# if 文
if [ "x$1" = "-h" -o "x$1" = "--help" ]; then
  echo "Usage: $0 [-h|--help]"
fi

# while 文
I=0
while [ $I -lt 5 ]; do
  echo $I
  I=`expr $I + 1`
done

cat list.txt | while read LINE; do
  echo $LINE
done

# &&を使った例
[ -s ./target.txt ] && echo "ok, file exists"

(小技)文字列判定時のダミー文字列

上のサンプルのようにifで引数を判断する場合に、

if [ "x$1" = "x-h" ]; then

とわざわざ x を入れることがシェルプログラムで多々あった。なぜか。
昔は、例えば上のプログラムだと引数が与えられず $1 が空文字列になった場合 x がないと if [ = "-h" ]; then と解釈されて文法エラーが起こってしまっていた。これを回避するために適当な文字列を入れるという伝統的な小技ある。が、今はコマンドのほうでこの問題は解消されているので x などを入れる必要はない。12

case

割と柔軟な分岐を作ることができる。
;; を忘れがちなので注意(; ;)。

while :
do
    echo -n "What'up?) "
    read K
    case $K in
        x)   echo "X-("
                           ;;
        "q") echo "Bye"             # "" で文字列評価してもOK
             break                                     # このbreakはループからの抜け出し
             ;;
        *shirabero)                 # *,? のメタ文字利用がOK
             echo "Nothing is there, Boss"
             ;;
        [yY] | yes | YES )          # 論理和, [abc][a-z][!0-9] もOK
             echo "Yeah!"
                          ;;
        [nN] | no | NO )
             echo "Oops..."
                          ;;
        *)   echo "What?"           # デフォルト. 順番に評価されるので最後に置くとよい.
             ;;
    esac
done

関数

関数はそれを呼ぶ場所より前に定義されていなければならない
呼び出し時は、カッコは不要。

関数とローカル変数

funcname() {
    local FUGA
    set -- $*
    HOGE=$1
    FUGA=$2    # FUGA はローカル変数
}

# Call
funcname
funcname "hoge" "fuga"

return

 返せる値は数値で0~255まで。演算結果返すのではなく、何か自前のステータスコードを返す目的である。


somefunc() {
   [ "x$1" = "x0" ] && return 1
}

(小技)eval を使って関数の返値を変数に代入

他の言語のように、代入式の右辺として使えないのがシェルの関数の短所。(つまりは、シェルのは関数ではなくサブルーチンである。)
しかし、次のようにevalを使って、擬似的な関数呼び出し+代入が行える。

getdate() {
    eval $1=`date '+%Y/%m/%d'`
}
getdate TODAY  # 変数TODAYに関数を用いて必要な値を代入
echo $TODAY    # ex) 2015/12/25

プログラムパターン

ファイル入出力

ここに書いた以外の使い方もあるが、最低限 |>>>、あと時々>&1>&2を覚えておけば大抵のことはオケマル。
リダイレクタなどの解説はこちらのQiita記事こちらのページ などを参考にされたし。

パイプ

コマンドの結果を縦棒|を使った パイプ という仕組みを使って次のコマンドに渡せる13。さらに次のコマンドへ…、と連続で渡せる。

pipe_sample
cat logfile.txt | grep '10.0.0.123'
cat logfile.txt | cut -d' ' -f 3 | sort | uniq -n | sort -rn

出力のリダイレクト

コマンドの実行結果(標準出力)を直接ファイルへと書き込むことを、 出力のリダイレクト という。14
1>(上書き) と 1>>(追記) を使う。1 は標準出力を意味するファイル記述子15。ただしここの 1 に限っては省略可能。デフォルトなのだろう。

コマンドのエラー(標準エラー出力)をファイルに書き込む場合には、同様に 2>2>> を使う。1>>>1と合わせて使うことも可能。

それぞれ省略された場合は、画面に出力される。

output_redirect_sample1
ls -la hoge*.txt        # リダイレクトが省略されているので結果とエラーは画面に出力される
df -gh > diskinfo.txt
tail -1 somefile.txt >> logfile.txt    # 追記(アペンド)
ping example.com > reachable.txt 2> unreachable.txt

次の例は、コマンドの実行結果もエラー結果も一つのファイルに出力する例。

output_redirect_sample2
sh somescript.sh >logfile.txt 2>&1    # 2>&1 >logfile.txt ではうまく行かないので注意

明示的に標準エラー出力に出力するのはどうすんだっけ? って、老化による記憶力低下が原因で時々なる。なのでここにメモする。

redirect_to_stderr_sample
echo "STDOUT"        # 標準出力へ
echo "STDOUT" >&1    # 標準出力へ(これはほぼ使われない)
echo "STDERR" >&2    # 標準エラー出力へ

/dev/null

Unix の /dev/null 16 は、即時消去のゴミ箱みたいな使い方もできるし、まっさらなプレーンファイルのような使い方もできる。
出力リダイレクトと組み合わせて使うことが多いので参考に。

dev_null_sample

myscript.sh >/dev/null 2>&1    # 標準出力と標準エラー出力を破棄する (crontable でよく用いられる使い方)
somecommand 2>/dev/null        # エラー出力だけ破棄する
cat /dev/null > myfile.txt     # サイズ0のファイルを作る
cp /dev/null > myfile.log      # (参考)既存のファイルのサイズを0にする(新規であれば上の例に同じ新規作成)

(小技)execをつかって標準出力を一時的にファイルに出力する

> の向きで主客を取り違えてしまうので注意かな。

exec 3>&1 >$tmpfile    # 標準出力(1) を ファイルディスクリプタ3 に複製
cat hogehoge.txt       # 任意のコマンド
exec 1>&3              # ファイルディスクリプタ3 を 1 に複製(標準出力を1に戻す)

入力のリダイレクト

ファイルの内容をコマンドの標準入力へ入力することを、 入力のリダイレクト と言う。
< を用いる。17

input_redirect_sample
mysql -h mydbhost.intranet -u myuser -p mydatabase < sqlfile.sql

ヒアドキュメント

<<EOFといった具合に終端文字列を指定してヒアドキュメント18を作成できる。(文字列EOF部分は任意)
書き方には種類があって、<<EOFと書く場合と、<<'EOF'または<<"EOF"とクォーテーションで囲む囲まないの違いがある。前者はヒアドキュメント部分でも変数展開は行い、後者は行わない。でもダブルクォーテーションなのに変数展開は行わないのはなんか気持ち悪いので、自分はシングルクォーテーションで囲むようにしている。

here.sh
#!/bin/sh

NOW=`date "+%Y/%m/%d %H:%M:%S"`

cat <<EOF
Now is $NOW
EOF

cat <<'EOF'
Now is $NOW
EOF

cat <<"EOF"
Now is $NOW
EOF
実行結果
$ ./here.sh 
Now is 2018/03/14 13:23:11
Now is $NOW
Now is $NOW

タブを削除するヒアドキュメント <<-EOF

<<-EOF のように - を入れることによって、引用行の冒頭のタブを削除することができる。
ただ、削除するのはタブ(\t、ハードタブ)なので、スペースを使ったインデントは削除しない。(なので昨今は使う機会が少ないと思う)

cat <<-EOF
	When you look at the dark side, careful you must be …
	for the dark side looks back.
EOF

ヒアストリング <<<

ヒアストリングはヒアドキュメントの派生で、文字列を渡す場合に使うことができる。

cat <<< "May the force be with you."

組み込みコマンドの活用

set --

setは、--オプションを使う事で、文字列を空白文字で分割して、位置パラメータにセットできる。

set -- $line    # line <= "hoge fuga"
CHUNK1=$1      # hoge
CHUNK2=$2      # fuga

なお、分割対象の文字列中に、Glob文字(*)があると、カレントディレクトリのファイル名が展開されてしまうので、これを無効にした場合は、-f オプションをつける。

line='* hoge'
set -f -- $line

getopts

getoptsで、コマンド実行時に渡されるオプションパラメータの解析が簡便にできる。

while getopts a:h OPT
do
    case $OPT in
        a) SOMEVAL=$OPTARG           # some command
            ;;
        h) help                      # go help subroutine
            ;;
        *) help
            ;;
    esac
done

shift $(( $OPTIND - 1 ))            # <- 記述を忘れがち!
TARGET=$1                           # $OPTIND-1 分オプション引数が捨てられている

read

コメントよりBuild-inのreadが複数変数を指定できることを教えてもらった。
read はいろいろなオプションも指定できる(GNU BASHマニュアル - 4.2 Bash Builtin Commands: read)。
サンプル中のシェル変数IFSは、分割子を指定している。

adding.sh
#!/bin/sh

cat somedata.csv | while IFS=',' read p1 p2 p3
do
    if [ "x$p1" = "x1" ]; then
        printf "%d + %d = %d\n" $p2 $p3 `expr $p2 '+' $p3`
    fi
done

## sample: somedata.csv 
#1,1,2
#0,1,1
#1,2,3

行をまるっと使う場合は、read専用シェル変数$REPLYを使う。ただし、readに指定する変数がない場合のみ有効。

musics.sh

cat musiclist.txt | while read
do
    set -- $REPLY
    if [ "x$1" = "x70s" ]; then echo $REPLY; fi
done

## sample: musiclist.txt
#70s Can't Take My Eyes Off You
#70s Dschinghis Khan
#90s Kiss Of Life
#90s Space Cowboy
#70s Moskau

 なお、パイプ経由でwhileへデータを渡すような書き方だと、whileがサブシェルで起動されるため、その外側で定義した変数は更新されない。forや外部ファイルの入力リダイレクト(<)、名前付きパイプなどで行うとよい。(参考元:「bash で,サブシェルが起動される条件」)

db-saiyans.sh
#!/bin/sh

COUNT=0
cat dbdata.txt | while read NAME FLAG VALUE
do
    [ "x$FLAG" = "x1" ] && COUNT=`expr $COUNT + $VALUE`
    echo " - $COUNT"
done
echo "HAGE POWER LEVEL TOTAL: $COUNT"     # 0 

## sample: dbdata.txt 
#Gohan 0 2800
#Piccolo 1 3500
#Krillin 1 1770
db-saiyans2.sh
#!/bin/sh

# 入力リダイレクトを使った方法
COUNT=0
while read NAME FLAG VALUE
do
    [ "x$FLAG" = "x1" ] && COUNT=`expr $COUNT + $VALUE`
done < dbdata.txt
echo "HAGE POWER LEVEL TOTAL: $COUNT"     # 合算されている
db-saiyan3.sh
#!/bin/bash

# 名前付きパイプを使った方法( bash専用 )
COUNT=0
while read NAME VALUE
do
    [ "x$FLAG" = "x1" ] && COUNT=`expr $COUNT + $VALUE`
done < <(cat dbdata.txt)
echo "HAGE POWER LEVEL TOTAL: $COUNT"     # 合算されている

 bash-hackers.org で紹介されていた read を用いた Yes/Noアンサー を得るサンプルコードです。

asksure.sh
#!/bin/bash

asksure() {
  echo -n "Are you sure (Y/N)? "
  while read -r -n 1 -s answer; do
    if [[ $answer = [YyNn] ]]; then
      [[ $answer = [Yy] ]] && retval=0
      [[ $answer = [Nn] ]] && retval=1
      break
    fi
  done
  echo # just a final linefeed, optics...
  return $retval
}

### using it
if asksure; then
  echo "Okay, performing rm -rf / then, master...."    # おいw
else
  echo "Pfff..."
fi

[[ ]] はBashの構文で、基本的には [ ] と同じ。ただし、[ ]とは違って&&||など、拡張した表現が使える。こちらの別記事でまとめてみた

trap

シグナルごとに設定可能。といっても使うシグナルは限られているか。KILL(9)はキャッチ不可。

# trap 'command' sign-num 
trap 'echo "see you"' 0           # 0 ... exit
trap 'rm -f $tmpfile;exit 1' 2    # 2 ... INT(Ctrl+C)
trap 0                            # reset (no command, just 1 arg)

misc

シェル変数 $PIPESTATUS

自分はあまり使わないので、詳しく書いているリンクだけ掲載。

コマンドパイプラインの終了コード - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

スクリプト自身のディレクトリの取得

ここの解説を参考にした。

bash
SCRIPTDIR=$(cd $(dirname $BASH_SOURCE); pwd)

デバッグTips

スクリプトを実行するときに、明示的にshまたはbashコマンドにオプションをつけて実行することで、簡単なデバッグか可能である。

シンタックスチェックをする -n オプション

-n オプションをつけて実行することで、シンタックスチェックをしてくれる。問題なければ何も出ない。
ただ、残念だがそれほど精度は高くない。コマンドのtypoがあったとしても、実行時にしか見つけられないので、通ってしまう。

なお、 -v オプションを共に用いることで、スクリプトのチェック内容が表示される。

$ sh -n test.sh    # if~fi や do~done での閉じ忘れの場合
test.sh: line 10: syntax error: unexpected end of file

実行結果を都度表示してくれる-xオプション

明示的にshまたはbashコマンドに -x オプションをつけて実行することで、コマンドの実行結果をいちいち表示してくれる。コマンドの実行結果や式の様子をトレースできるので、だいぶ楽になるだろう。

$ sh -x db-saiyans2.sh
+ COUNT=0
+ read NAME FLAG VALUE
+ '[' x0 = x1 ']'
+ read NAME FLAG VALUE
+ '[' x1 = x1 ']'
++ expr 0 + 3500
...

なお、 これも -v オプションで、そのコマンド内容が表示されるので、共に用いると良い。

上書きを抑止する -C オプション

大文字のCを使った -Cオプション(または set -C)を使うと、存在しているファイルへのリダイレクト > を使った上書きを禁止できる。>> による追記は可能。また、この上書き禁止状態の場合に使える特殊なリダイレクト >| を使うことで上書きができる。
ファイルが存在しない場合には > をつかっても >> を使っても新しいファイルが作られる。

noclobber.sh
#!/bin/sh
  
touch hoge fuga
set -C
echo "hoge" > hoge     # エラー. 上書き禁止のため
echo "fuga" >> fuga    # OK.
echo "piyo" >| hoge    # OK. (特殊な >| の使用)

echo "boo" > boo       # OK.
echo "poo" >> poo      # OK.

問題があった場合に実行を止めてくれる-eオプションと-u

-eオプション(または set -e)を使うと、エラーがあったときに処理が止まるようになる。
…のだが、ちょっとクセもあり使い所を選びたい。僕は覚えないでBashの習熟を進めていたので今でも使っていない。悪しからずご了承を。このToggeter記事も参考に。

-uオプション(または set -u)を使うと、未定義の変数を使うとエラーとなり、その時点で処理が止まる。デバッグやテストでは有用だが、本番では使うべきではないと思う。そうならないように設計しエラーハンドリングを実装し、テストをしたほうが良いから。

コマンドTips

シェルスクリプト構文ではないけど、スクリプト内で使うと便利なコマンド関連Tips。

tar と gzip を使ったパイプ処理による圧縮

gzip-c オプションで、標準入力・出力とで圧縮対象データのやり取りをする。
tar- で、対象を標準入力・出力にすることができる。
もちろん、 bzip2 bunzip2 などで同じことが可能。

$ # making tarball & gzipping
$ tar cvf - target_dir | gzip -9c > target_dir.bkup20160704.tar.gz
$ # ungipping & expand tarball
$ gunzip -c target_dir.bkup20160704.tar.gz | tar xvf -

もちろん、Linuxなどが使っているGNU gzipなら、gzipのオプションで、最初から圧縮されたTarBall .tgz ファイルが作れる。

date コマンドによる日付のパース

date --date="$somestr" '+%Y/%m/%d'
# not only date string but also: today, yesterday, last month, etc.

curl

「カール」と呼ぶのはだいぶ後になって知りました//

curlにパイプ経由でデータを渡す

'-d' オプションで、引数に@-を渡す。

$ cat text.txt | curl -sS -X POST -d @- http://example.com/boopoo

これを利用することで、シェルからJSON情報をPOSTで渡せる。

$ cat <<'EOF'  | curl -sS -X POST -d @- http://example.com/boopoo
{
  "a": 123,
  "b": [
    4,5,6
  ]
}
EOF

まぁ、 joコマンドを導入してもよいかもね。

$ jo a=123 b=$(jo -a 4 5 6) | curl -sS -X POST -d @- http://example.com/boopoo

算術計算の expr と (( ))

コマンド expr を使う方法と、 Bash であれば (( )) 式を使う方法がある。

expr を使う方法は古の時代からある方法で、shしか使えないOSでも使われていた19。計算したい式を引数にして渡す。

expr_fizzbuzz.sh
#!/bin/sh

C=1
while [ $C -le 100 ];
do
    if [ `expr $C % 15` -eq 0 ]; then
        echo "FizzBuzz"
    elif [ `expr $C % 3` -eq 0 ]; then
        echo "Fizz"
    elif [ `expr $C % 5` -eq 0 ]; then
        echo "Buzz"
    else
        echo $C
    fi
    C=`expr $C + 1`
done

Bashの場合であれば、(( ))が使える。(代入する場合の右辺として使う場合は $(( )) とする。また(( )) の中では変数は$で始めなくてもOK。)
こちらの記事にもまとめているのでご参考あれ。

double_parantheses_fizzbuzz.bash
#!/bin/bash

C=1
while (( C<=100 ))
do
    if (( !(C%15) )); then
        echo "FizzBuzz"
    elif (( !(C%3) )); then
        echo "Fizz"
    elif (( !(C%5) )); then
        echo "Buzz"
    else
        echo $C
    fi
    C=$(( C + 1 ))
done

# (( )) をつかうと、for をC言語のように記述できるので、上のwhileは
# このように置き換えることも可能。
# for (( C=1; C<=100; C++ ))
# {
#     ...
# }

自分のグローバルIPアドレスの調べ方

http://ifconfig.io 20 にcurlでアクセスすると、シンプルに自分のグローバルIPを教えてくれる。

example
$ curl ifconfig.io
1xx.1.23.45

とはいえ、個人運営のサービスであると思うので、利用は自己責任で21

改行コードを改行コード文字列に

なんのこっちゃかもだが、改行コードを¥nという文字列にするということ。Perlで。

$ cat some.txt | perl -pe 's/\n/%5Cn/'

文字コード変換

iconv を使うと良い。

# shift_jis で作られたファイル shift_jis.txt を UTF-8 に変換して出力する
$ iconv -f SHIFT_JIS -t UTF-8 shift_jis.txt
$ curl -sS http://example.com/shift_jis_contents | iconv -f SHIFT_JIS -t UTF-8

Macだとあるあるなのが、UTF-8で作られたCSVファイルがExcelで見れない(参考記事)
上の例と逆のことをすれば良い。
ただし、 -c オプションをつけるとよい。そうでないと、変換できない文字に遭遇した場合に処理が止まってしまうので。

$ iconv -c -f UTF-8 -t SHIFT_JIS csv_file.csv > csv_file_shiftjis.csv

エスケープされた日本語文字列のデコード

\uXXXXのようにUnicodeエスケープされた文字列をさくっとアンエスケープしたい時のワンライナー。とりあえずPerlとPythonを使ったの例。
ただし、サロゲートペア未対応。

# ほげ
echo "\u307b\u3052" | perl -lpe 'binmode(STDOUT,":utf8");s/\\u([0-9a-fA-F]{4})/chr(hex($1))/eg'
# ふが
echo "\u3075\u304c" |  python -c 'import sys; print(sys.stdin.buffer.readline().decode("unicode-escape"))'

同じコードを繰り返す

watch を使うと状態変化があるものをモニタリングのように集中して観察する場合などに便利。
集中して確認するので、画面はクリアされる。
なお、Macには最初から入っていないので、Homebrewなどでインストールの必要あり。

watch -n 1 date

連続して何か出力したいのであれば、 while を使うとよい。 do の後にコマンドを書けば良い。
なお sleep などで適当にアイドルを挟んだ方がよいだろう。

while true; do date; sleep 1; done

ちなみに $RANDOM は 0~32767までのランダムな整数が得られる。

# 擬似サイコロ
while true; do echo "$RANDOM*6.0/32767+1" | bc; sleep 1; done

(おわり)

  1. 加えてUNIXファイルシステムとパイプと哲学のおかげでもある。

  2. もともとSolarisから入ったsh派なのでのでBashで何が使えるんだっけ?とか、Bash独自の機能について忘れがちだった経緯がある。

  3. 時々参照していたBASHのすごく詳細解説のWikiサイトでしたが、2023/7現在Parking Domainになっている模様。。。なので、リンクを外しておきます。(参考:Redditの投稿

  4. 自分は「シェバング」と呼ぶ派。Wikipedia では「[シバン](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%90%E3%83%B3_(Unix) シバン - Wikipedia)」で書かれている。pingの読み方と同じように方言化されていてちょっと面白い。なおシェバングについて、これで指し示すコマンドをPATH環境変数に依存させるようにするならば、 envコマンドを使う手もある

  5. Ubuntu では dash へのリンクだそうだ。

  6. 色々な現場でコードを見てきたが、定数・変数の区別なく伝統的に大文字を使っていることが多かった。コマンドがほぼ小文字で書かれるため、可読性目的だったのではないかと推測。最近でこそ他言語のように定数・変数の命名規則について耳にすることもあるが、シェルスクリプトであまりそこをこだわっても綺麗に整理できない場合も多いので、伝統的なルールで良いのではないか、と思う。(そもそも命名規則まで持ち出すほど複雑化するようなスクリプトならば、他の言語の選択を検討せよ。)

  7. PHPやPerl、Rubyなどはこの文法が参考にされている。 2

  8. 組み込み側のマニュアルは、man bashを参照するとよい。なお、外部コマンドのほうは処理速度は遅くなる。

  9. ただ、コマンドによっては、別なことを意味する場合がある。例えばdiffではExit status is 0 if inputs are the same, 1 if different, 2 if trouble.である。

  10. 英語圏ではbacktick となることも多い。プログラミング用語として用いる場合はどちらも同じ意味

  11. こちらのに解説のQiita記事『(シェルスクリプトの [ は /bin/[ と言ったり [ "x$var" = "xval" ] と書く人はオジサン)[https://qiita.com/ko1nksm/items/f772b782a760bbfa1b28]』
    を参考にされたし。僕もオジサンなのですごく参考になった。

  12. "x-hack"というらしい。これもまたQiita記事『(シェルスクリプトの [ は /bin/[ と言ったり [ "x$var" = "xval" ] と書く人はオジサン)[https://qiita.com/ko1nksm/items/f772b782a760bbfa1b28]』を参考に。今はエラーが出ないのは私も知らなかった…私もオジサンなのを再確認したわ。

  13. パイプは、パイプラインともいう。UNIXの伝統的な機能である。正確に言えば、コマンドの標準出力の内容を、次のコマンドの標準入力へ渡す仕組みである。名前の由来はもちろん水道管みたいなパイプだろう。

  14. パイプと違って、標準出力の内容を、コマンドではなく、ファイルやデバイス(例えば画面やプリンタ)へ送るのがリダイレクト。リダイレクションとも。

  15. ファイルディスクリプターとも。MS-DOSではファイルハンドル。

  16. /dev/null は、特別なデバイスだ。

  17. ファイルからの入力リダイレクトは cat somefile | somecommand と同じ結果にはなる。立ち上がるプロセスが少ない分、単純なファイル入力はリダイレクトを使った方が、OSリソース的にはお得かもしれない。

  18. 英語では here-document。 その機能から hear-document だと思っていた時期が俺にもありました。

  19. Soralis8 とかは、デフォルトではshしか使えなかったなぁ。

  20. ifconfig コマンドをオマージュしたURLで面白い。

  21. たぶん作者はこの人かな。当然ながらプログラムに組み込むなどはしないほうがよい。あくまで確認目的。

1288
1310
10

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1288
1310

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?