Shell Script 入門

Basic

制御構文

  • if
i=10
if [ $i -le 5 ]; then
    i=`expr $i + 5`
elif [ $i -le 10 ]; then
    i=`expr $i + 10`
else
    i=`expr $i + 1`
fi
  • for
max=10
for ((i=0; i < $max; i++)); do
    echo $i
done
to_val=`expr 30 - 10 + 1`
for i in `yes "" | cat -n | head -30 | tail -$to_val`; do
    :
done
  • while
i=10
while [ $i -le 30 ]; do
    i=`expr $i + 5`
done
ls file* |
while read f ; do
   cp $f $f.backup
done

read varのようにすると入力を受付け、変数varに格納される。

  • case

/etc/rc0.d/S01halt

case "$0" in
   *halt)
        message=$"Halting system..."
        command="/sbin/halt"
        ;;  
   *reboot)
        message=$"Please stand by while rebooting the system..."
        command="/sbin/reboot"
        kexec_command="/sbin/kexec"
        ;;  
   *)  
        echo $"$0: call me as 'halt' or 'reboot' please!"
        exit 1
        ;;  
esac

条件判定部分は正規表現が使える。

  • until
i=10
until [ $i -le 5 ]; do
    i=`expr $i - 1`
    echo $i
done

特殊変数

  • $#
    • コマンドラインの引数
  • $1$9, $0
    • 引数それぞれ。 Positional Parameters という。
  • $*
    • $0 以外のコマンドライン引数
  • $@
    • $* と類似。ただし $@ とした時、位置パラメータを評価せずにコマンドに渡すことが出来る
  • $?
    • シェルが最後に実行したコマンドの終了状態を保持している。ほとんどのコマンドは成功時には0を返す
  • $$
    • 現在のシェルのプロセス番号を保持している
  • $-
    • シェルにセットされているオプションを保持している
  • $!
    • バックグラウンドで実行された直前のプロセスのプロセス番号を保持しています
  • Reference
  • Title: Shell 特殊変数
    • URL: https://qiita.com/a_yasui/items/ec4f75b300410af8958d

文字列

文字列操作

  • 文字列の一部抜粋

    • 左から : cut -c -7
    • 右から : cut -c ``expr ${#var} + 1 - 7``-
    • 左右から : cut -c 9-13
  • 大文字・小文字変換

    • tr "a-z" "A-Z"
  • 正規表現でマッチした文字列の取り出し

    • AWK : matchstr=echo "STRING" | awk '{match($0, /PATTERN/); print substr($0, RSTART, RLENGTH)}'
    • SED : matchstr=echo "STRING" | sed 's/.*\(PATTERN\).*/\1/'
    • GREP: matchstr=echo "STRING" | grep -o 'PATTERN'
  • 特殊文字のトリミング

    • 左側のトリミング後、右側のトリミング

    • タブやスペースを取り除きたい場合は、trimming_char=printf " \t"

      string="---abc-defgh----"
      trimming_chr="-"
      
      while [ "_$string" != "_${string#[$trimming_chr]}" ]; do
          string="${string#[$trimming_chr]}"
      done
      while [ "_$string" != "_${string#[$trimming_chr]}" ]; do
          string="${string%[$trimming_chr]}"
      done
      
  • ファイル名・ディレクトリ名の取得

    • ファイル名取得
      • basename
      • filename="${filepath##*/}" # 左側からの最大マッチング
    • ディレクトリ名取得
      • dirname
      • dirpath="${filepath%/*}" # 右からの最小マッチング

文字列抽出

  • ${var:-word} : $varが未定義か空文字の場合は文字列wordが読み出される

  • ${var-word} : $varが未定義の場合は文字列wordが読み出される

  • ${var:=word} : $varが未定義か空文字の場合は文字列wordが読み出され、かつ$varにも代入

  • ${var=word} : $varが未定義の場合は文字列wordが読み出され、かつ$varにも代入

  • ${var:?word} : $varが未定義か空文字の場合は文字列wordが読み出され、かつエラーの扱い

  • ${var?word} : $varが未定義の場合は文字列wordが読み出され、かつエラーの扱い

  • ${var:+word} : $varが未定義でも空文字でもなければ、文字列wordが読み出される

  • ${var+word} : $varが未定義でなければ、文字列wordが読み出される

  • $#{var} : $varの文字数

  • ${var#PATTERN} : 左端からPATTERNについて最小マッチングで切り落とされて読み出される

  • ${var##PATTERN} : 左端からPATTERNについて最大マッチングで切り落とされて読み出される

  • ${var%PATTERN} : 右端からPATTERNについて最小マッチングで切り落とされて読み出される

  • ${var%%PATTERN} : 右端からPATTERNについて最大マッチングで切り落とされて読み出される

  • ${@:2} : 二つ目以降の引数を取得

  • Reference
  • Title: Shell 特殊変数
    • URL: https://qiita.com/a_yasui/items/ec4f75b300410af8958d

文字クラス

一部の環境では動作せず、使わないほうが無難

  • [[:alnum:]] : 英数字。[0-9A-Za-z]
  • [[:alpha:]] : 英字。[A-Za-z]
  • [[:digit:]] : 数字。[0-9]
  • [[:lower:]] : 英字の小文字
  • [[:upper:]] : 英字の大文字。[A-Z]
  • [[:blank:]] : スペースとタブ
  • [[:punct:]] : 記号 ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ `` { | } ~
  • [[:xdigit:]] : 16進数。[0-9A-Fa-f]

(参考) 正規表現

メタ文字セット

BRE(基本正規表現)

  • 置換後文字列用
    • \n : n番目の\(\)で囲まれた範囲にマッチした文字列
    • & : マッチした文字列全体
    • \x : メタ文字&またはsed等で正規表現の始まりを住めすために用いた文字自身を指定したい場合
    • \\ : \を指定する場合

(参照) どのUNIXコマンドでも使える正規表現

ERE(拡張正規表現)

  • BREのときと比較して\をつけないなど、他多数

Others

ファイルの新規作成と追記

$ echo テスト > file
$ echo テスト >> file

ヒアドキュメント

$ cat << FIN > file
abc
def
FIN

ヒアストリング

$ A=テスト
$ sed 's/テ/ア/' <<< $A

# 以下の処理と同等
$ echo $A | sed 's/テ/ア/'

終了ステータス

$?

0で正常終了。正常ではないときは0以外の数字が終了ステータス。 テストコマンドでは正なら0、偽なら1、変な引数が指定されたら2のようにそれ以外の値が返される。

パイプで処理すると途中の処理のステータスが配列として格納される。bashで配列を扱うことができる機能

${PIPESTATUS[@]}

あるファイルがなければシェルスクリプトを終了するという処理の場合、以下のようにして書くことができる。

[ -f "/etc/passwd" ] || exit 1

引数

  • コマンド名 -- -引数のように--を指定することで、これ以降の-引数はオプションではなく、引数として解釈する

    • 実行例
      case "${arg}" in
      "--help") set -- "$@" "-h" ;;
      "--version") set -- "$@" "-V" ;;
      "--"*)
          echo "Failure: ${arg}"
          ;;
      *)
          set -- "$@" "${arg}"
          ;;
      esac
      
  • 引数を処理する

AWKとsed

AWK

  • 関数
    • printf
    • sprintf
    • sub
    • gsub
    • gensub
    • length(t)
    • split(s,a,fs)
    • substr(s,p,n)
    • index(s,t)
    • match(s,r)
    • tolower(s)
    • toupper(s)
  • 変数
    • ARGC
    • ARGV
    • ENVIRON
    • FILENAME
    • FNR
    • FS
    • NR
    • OFS
    • ORS
    • RS
$ seq 1 10 | xargs -n 5 > data
cat data
1 2 3 4 5
6 7 8 9 10
$ cat data | awk '{print $2,$4}'
2 4
7 9
$ cat data | awk '{a=3;print $(1+a)}'
4
9

NFは各行の列数を表す予約語

$ cat data | awk '{print $(NF-1)}'
4
9
$ cat data | awk '$4>6'
6 7 8 9 10
$ cat data | awk '$5==5'
1 2 3 4 5
$ cat data | awk '$5=="5"'
1 2 3 4 5
$ echo {a..g} | xargs -n 1
a
b
c
d
e
f
g
$ echo {a..g} | xargs -n 1 | awk 'NR>=4{print $1,$1,$1}'
d d d
e e e
f f f
g g g
$ echo {a..g} | xargs -n 1 | awk 'NR>=4{print $1,$1,$1}NR<=4{print $1,$1}'
a a
b b
c c
d d d
d d
e e e
f f f
g g g

標準入力より前処理、後処理

$ seq 1 5 | awk 'BEGIN{a=100000}{a+=1}END{print a}'
100005
$ echo {1..5} | awk 'BEGIN{a=100000}{for(i=1;i<=NF;i++){a+=$i}}END{print a}'
100015

文字列を表示

$ seq 1 3 | awk '{printf("%d円\n",$1)}'
1円
2円
3円

フォーマットを指定して表示。標準出力はしないため、一旦変数に文字列を書き出して出力

$ echo {a..z} | awk '{$9=sprintf("%s%s",$9,$9);print}'
a b c d e f g h ii j k l m n o p q r s t u v w x y z
# Once per one line
$ echo abcdfabcde | awk '{sub(/cd/,"xy",$0);print}'
abxyfabcde

# Many times per one line
$ echo abcdfabcde | awk '{gsub(/cd/,"xy",$0);print}'
abxyfabxye

# Once per line, return converted strings
$ echo abcdfabcde | awk '{$0 = gensub(/cd/,"xy",$0);print}'
abxyfabcde

3行目のみ表示

$ echo {a..e} | xargs -n 1 | awk "NR==3{print \$0}"
c

3行目から4行目まで表示

$ echo {a..e} | xargs -n 1 | awk "NR==3,NR==4{print \$0}"
c
d

cの行からdの行まで表示

$ echo {a..e} | xargs -n 1 | awk "/c/,/d/{print \$0}"
c
d

CSVファイルの第3フィールドの合計値を求める場合

$ total_size=`awk -F "," "{T=T+\\$3} END {print T}" "$filename"`

-F,のように指定すると文字列で区切る

sed

3行目のみ置換

$ echo {a..e} | xargs -n 1 | sed '3s/./???/'
a
b
???
d
e

2行目から最終行まで置換

$ echo {a..e} | xargs -n 1 | sed '2,$s/./???/'
a
???
???
???
???

bの行からdの行まで置換

$ echo {a..e} | xargs -n 1 | sed '/b/,/d/s/./???/'
a
???
???
???
e

指定した範囲(3行目から4行目を表示)。sedとしては入力された行をそのまま出力するのが基本動作なので-nを指定することで指定範囲の行が2行ずつ表示されることを抑えている

$ echo {a..e} | xargs -n 1 | sed -n '3,4p'        
c
d

-nを指定しない場合

$ echo {a..e} | xargs -n 1 | sed '3,4p'   
a
b
c
c
d
d
e
  • xargsは標準入力から読み込んだ文字列をしていしたコマンドに引数として渡すコマンド
    • -I@のようにオプションで文字を指定すると、別途その文字を指定した位置に引数として渡す
    • -n 1のようにいくつの引数を渡すかを指定
    • -P 5のように何並列でプロセスを立ち上げるかを指定。0を指定するとできるだけプロセスを使うように指定

jq

jq Manual (development version)

  • -rでダブルクオテーションを削除
  • []{}で囲むと配列形式やオブジェクト形式で出力可能
  • | @csvのように渡すとCSV形式で出力可能
  • | {InstanceId, Tags:(.Tags|from_entries)} | select(.Tags.Name | contains("test"))'のようにすることでタグ指定で対象の項目を取得。Tags:の部分は出力結果の表示名。from_entriesで"Key": "auto-stop""Value": "yes"のような形式を"auto-stop": "yes"にまとめる
  • | lengthで長さ取得
  • | unique[]で配列に対して、重複排除。最後に[]をつけない場合、配列で結果表示
  • | fromjsonで文字列化されたJSONを復元
  • jq '.events[] | .timestamp/1000 | todate'のように秒単位にした上で| todateを使用すると時間の表示形式を整形
  • jq '.Functions[] | {FunctionName, VpcId:(.VpcConfig.VpcId//"NoVPC")}'のように//を指定すると左側がNULLのときは右側の要素に置き換える。
  • jq -r '.Reservations[] | .Instances[] | [.InstanceId, (.Tags | if (.==null) then null else ([.[] | "\(.Key)=\(.Value)"] | join(",")) end)] | @csv'のようにif () then ~ else ~ endを仕様できる。また、join()で指定文字列で文字列の配列を1つの文字列に結合
$ aws ec2 describe-instances | jq '.Reservations[] | .Instances[] | .InstanceId'
"i-0a6b646d73a79370a"
"i-096d0b925a6d835d5"
"i-07ee081ad9075e2f7"
"i-045b56c08c46b11bd"
"i-043890efa180c22a2"
"i-0048d288f55da347d"
"i-0d79cc953246cb398"
"i-0e130f4914a5938a0"
"i-079f1057910324a39"

デバッグ

  • デバッグ
    • 未定義変数参照時にエラー
      • スクリプトの冒頭に#!/bin/sh -uのように-uオプションを付与
      • set -uを宣言
  • トレース
    • スクリプトの冒頭に#!/bin/sh -xのように-xオプションを付与
  • デバッグメッセージの分離
    • FIFOパイプ
      • mkfifo /tmp/pipeしたらecho "test" > /tmp/pipeのように書き出す
      • 別端末からwhile [ 1 ];do cat /tmp/pipe;done
  • パイプの中身
    • 途中でtee /dev/stderrにリダイレクトすることやFIFOパイプを利用

Others

grepでタブ記号が入っている場合の検索

タブ記号をスペースに置き換える。[:space:]は文字クラスを表し、タブや半角スペースなどの空白文字を指す

$ cat /etc/services | grep http | tr '\t' ' ' | grep ' 80/'
http             80/udp     www www-http # World Wide Web HTTP
http             80/tcp     www www-http # World Wide Web HTTP

$  cat /etc/services | grep http | grep '[[:space:]]80/' 
http             80/udp     www www-http # World Wide Web HTTP
http             80/tcp     www www-http # World Wide Web HTTP

grepはデフォルトで基本正規表現。-Eオプションで拡張正規表現が利用でき、{5}で5回繰り返すといった表現が可能になる。

  • wcコマンド
    • -m : ロケールを考慮した文字数カウント
    • -l : 行数カウント

sed 's/./&\n/g'で文字ごとに改行できるので、行数を数えることでも計算可能。 grep -o .で任意の一文字を検索して一行ごとに出力。

$ echo 123456789 | sed 's/./&\n/g'
1
2
3
4
5
6
7
8
9
$ echo 123456789 | sed 's/./&\n/g' | wc -l
10
  • unique -c : カウント

<()はプロセス置換でbashの機能

$ diff <( cd /var/log/test1 ; find ) <( cd /var/log/test2 ; find )

HTMLの編集

$ cat test.html
<!DOCTYPE html>
<!-- saved from url=(0022)https://hayashier.com/ -->
<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
		<!-- Global site tag (gtag.js) - Google Analytics -->
		<script src="./test_files/osd.js"></script><script type="text/javascript" async="" src="./test_files/analytics.js"></script><script src="./test_files/f.txt" id="google_shimpl"></script><script async="" src="./test_files/js"></script>
		<script>
		  window.dataLayer = window.dataLayer || [];
		  function gtag(){dataLayer.push(arguments);}
		  gtag('js', new Date());
:

コメントアウト削除

$ cat test.html | sed 's/<!--/\n&\n/g' | sed 's/-->/\n&\n/g' | sed '/<!--/,/-->/d'
<!DOCTYPE html>
		<script src="./test_files/osd.js"></script><script type="text/javascript" async="" src="./test_files/analytics.js"></script><script src="./test_files/f.txt" id="google_shimpl"></script><script async="" src="./test_files/js"></script>
		<script>
		  window.dataLayer = window.dataLayer || [];
		  function gtag(){dataLayer.push(arguments);}
		  gtag('js', new Date());

タグ削除。以下、実体参照が正しく使用されている場合

$ cat test.html | sed 's/<!--/\n&\n/g' | sed 's/-->/\n&\n/g' | sed '/<!--/,/-->/d' | sed 's;<[^<]*>;;g' | awk 'NF!=0'
		  window.dataLayer = window.dataLayer || [];
		  function gtag(){dataLayer.push(arguments);}
		  gtag('js', new Date());

ファイルの同期

$ rsync -av ~/ --exclude='.Trash' 192.168.5.1:~/home/hayashier/

sedでまとめて置換

sedで置換するルールを予めファイルに列挙しておいてまとめて置換することができる

$ cat /var/log/apach2/access.log* | ./month.sed | tail -n 1
#!/bin/sed -f

s/Jan/01/
s/Feb/02/
s/Mar/03/
s/Apr/04/
s/Mar/05/
s/Jun/06/
s/Jul/07/
s/Aug/08/
s/Sep/09/
s/Oct/10/
s/Nov/11/
s/Dec/12/

sedで部分一致箇所の抜粋

()で囲っておくと、\1のようにして取り出せる

$ echo 'A B C [D] "E" F G " "H" "I"' | sed 's/^\(.*\) \(.*\) \(.*\) \[\(.*\)\] "\(.*\)" \(.*\) \(.*\) "\(.*\)" "\(.*\)"$/\1|\2|\3|\4|\5|\6|\7|\8|\9/'
A|B|C|D|E|F G|"|H|I

Apacheのアクセスログの分析例

  • 改行やスペースを除く、文字でないバイナリを除去する
  • データ内の区切り文字以外の"や区切り文字を含んでいてっもフィールドを区切れるようにバックスラッシュは\\と記録されているので、これを%5Cにエンコーディング
  • "\"と記録されているので、これを%22にエンコーディング
#!/bin/bash -xv

B='\(.*\)'
D='"\(.*\)"'
P='\[\(.*\)\]'
STR='\1\x0\2\x0\3\x0\4\x0\5\x0\6\x0\7\x0\8\x0\9\x0'

sed 's;\\\\;%5C;g' < /dev/stdin             |
sed 's;\\";%22;g'                           |
sed "s/^$B $B $B $P $D $B $B $D $D\$/$STR/" |
sed 's/_/\\_/g'                             |
sed 's/ /_/g'                               |
sed 's/\x0\x0/\x0_\x0/g'                    |
sed 's/\x0\x0/\x0_\x0/g'                    |
tr '\000' ' '                               |
sed 's/ $//'

IPアドレスのソート

$ cat ip
192.168.1.2
10.245.0.1
10.95.20.1
40.1.212.4
203.113.10.1
$ sort -t . -k1,1n -k2,2n -k3,3n -k4,4n ip
10.95.20.1
10.245.0.1
40.1.212.4
192.168.1.2
203.113.10.1

以下の書籍等を参考にさせていただき、自分用の備忘録にまとめました。

Others

My Twitter & RSS

Leave a Reply

Your email address will not be published. Required fields are marked *