Tech Blog

シェルの色々な機能

2021-03-02

数値の計算

シェルは変数を文字列として取扱い、数値としては考えません。

数字であってもそれは数値ではなく文字として扱われているだけです。

計算処理を行うにはexprコマンドを利用します。

整数の計算

expr 1 + 1
2
expr 1 - 2
-1
expr 2 \* 2
4
expr 4 '*' 3
12
expr 4 / 2
2
expr 4 % 3
1

掛け算の*は必ずクォーテーションをつける必要があります。

変数の利用

変数を使って計算させた数値を元の変数に代入する例です。

VALUE=1
VALUE=`expr $VALUE + 1`
echo $VALUE
2

数値の比較

数値の大小を比較する場合はtestコマンドが利用できます。

test int1 -eq int2
test int1 -ne int2
test int1 -lt int2
test int1 -le int2
test int1 -gt int2
test int1 -ge int2

浮動小数点を含む計算

浮動小数点を含む計算を行う場合はbcコマンドが利用できます。

小数第何位まで出力するかをscale=nで指定します。

echo "scale=n; num1 + num2" | bc # num1にnum2を加える
echo "scale=n; num1 - num2" | bc # num1にnum2を引く
echo "scale=n; num1 * num2" | bc # num1にnum2を掛ける
echo "scale=n; num1 / num2" | bc # num1にnum2を割る
echo "scale=3; 10 / 3" | bc
3.333
echo "scale=4; 10 / 3" | bc
3.3333
echo "scale=3; 3.33 / 3.1234" | bc
1.066
echo "scale=7; 3.33 / 3.1234" | bc
1.0661458
echo "3.5678 / 3" | bc
1
echo "scale=2; 3.5678 / 3" | bc
1.18

数値かどうかの判定

cat is_numeric
#!/bin/sh
NUMBER=$1
expr "$NUMBER" + 1 > /dev/null 2>&1
if [ $? -lt 2 ]; then
	echo "Numeric"
else
	echo "Not Numeric"
fi

計算結果(標準エラー出力の結果を標準出力にマージしたもの) を/dev/nullに捨てます。

$?はコマンド実行時の終了ステータスを表す関数で、

$NUMBERの値が数値であれば、$?には終了ステータス0(計算結果が0の場合のみ1)が代入されます。

また、$NUMBERの値が数値でなければ、、$?には2が代入されます。

上記スクリプトの実行例です。

./is_numeric 3
Numeric
./is_numeric
Not Numeric

カラムに対する計算

COLUMN=5
ls -l | awk '{total += $'$COLUMN'} END {print total}'

awkは1行ずつ行を読み込み処理します。

5番目のカラムの値を足していきます。

ENDの後は、全部の行を処理した後に実行されます。

ls -l
total 24
-rw-r--r--  1 jun  staff  17  3  2 22:28 aaa
-rw-r--r--  1 jun  staff   5  3  2 22:28 bbb
-rw-r--r--  1 jun  staff  24  3  2 22:28 ccc
COLUMN=5
ls -l | awk '{total += $'$COLUMN'} END {print total}'

文字列の操作

フィルタを使った文字列処理

STRING="abc def ghi"
STRING=`echo "$STRING" | sed -e "s/def/xyz/g"`
echo "$STRING"
abc xyz ghi

echo "$STRING" | sed -e "s/def/xyz/g"で出力された文字列abc xyz ghiが 標準出力に出力されますが、その値をSTRINGに再代入しています。

実行した結果をバッククォートで囲むことで、元の変数に代入できます。

文字列の連結

文字列を連結するだけなら変数を並べて書くだけです。

STRING1=abc
STRING2=xyz
VAR=$STRING1$STRING2
echo $VAR
abcxyz

変数+文字列とする場合は中括弧でその変数部分を囲む必要があります。

STRING=abc
VAR=${STRING}xyz
echo $VAR
abcxyz

文字列 + 変数ならば中括弧は不要です。

STRING=xyz
VAR=abc$STRING
echo $VAR
abcxyz

余計なホワイトスペースの削除

変数にホワイトスペースが混在していても、echoコマンドで出力する際に シェルが余計なスペースを取り除いてしまうので、余分なホワイトスペースを削除できます。

STRING=`echo $STRING`

文字列の長さ

exprコマンドをexpr "string" : '.*'のように使うと文字列の長さを得られます。

exprは引数にコロン(:)があると、 右辺と左辺の文字列を比較して先頭から何文字までが等しいかという値を返します。

STRING=abcdefghij
NUMCHARS=`expr "$STRING" : '.*'`
echo $NUMCHARS
10

ただし上記のスクリプトでは、 STRINGexprコマンドで利用する演算子が1文字だけ含まれていた場合、 exprが演算子だと判断し、エラーとなってしまいます。

STRING="*"
NUMCHARS=`expr "$STRING" : '.*'`
echo $NUMCHARS
expr: syntax error

case文で任意の1文字の時のみ分岐をさせると演算子が含まれていても 期待された結果が得られます。

STRING='*'
NUMCHAR=`case "$STRING" in
  ? ) echo 1 ;;
  * ) expr "$STRING" : '.*' ;;
esac`
echo $NUMCHAR
1

文字列中にある文字列が含まれているか

grepコマンドは実行終了ステータスとして、 文字列が含まれていたら真、含まれていなければ偽を返します。 それを利用して、文字列中にある文字列が含まれているかの判定が可能です。

echo abcdefghijklmnopqrstuvwxyz | grep xyz > /dev/null
if [ $? -eq 0 ]; then
  echo "xyzが含まれます"
else
  echo "xyzは含まれていません"
fi

grepの実行結果は/dev/nullに捨てます。

ps -ax | grep jserver
#!/bin/sh
STRING=abcdefghijklmnopqrstuvwxyz
SUBSTRING=bcd
if echo "$STRING" | grep "$SUBSTRING" > /dev/null
then
  echo "Found it."
fi

case文を使ってチェックすることも可能です。

#!/bin/sh
STRING=abcdefghijklmnopqrstuvwxyz
SUBSTRING=bcd
case "$STRING" in
  *"$SUBSTRING"* ) echo "Found it." ;;
  * ) echo "Not Found" ;;
esac

文字列の中の一部分の切り出し

expr "abcdefghijklmn" : "a.*\(e..h\)i.*"
efgh
パターン説明
.*何もないものを含め、どんな文字列をも表す
.何か1個の文字(ヌル文字にも該当する)
*直前の文字が0個以上並んだ文字列
.ドット文字そのもの
*アスタリスクそのもの
具体的な例説明
abc.*abcで始まる文字列ならなんでも
.*abcabcで終わる文字列ならなんでも
ab*cacやabbbcというようにaとcの間にbが0個以上ある文字列
ab.*cabで始まってcで終わる文字列
a....baで始まってbでおわる6文字の文字列
..*.c.cで終わる文字列。.cの前に少なくとも1文字ある。
expr "string" : "pattern\(.*\)" # patternという文字列より後ろ
expr "string : "\(.*\)pattern" # patternという文字列より前
expr "string : "\(...\).*" # 初めの3文字
expr "string : ".*\(...\)" # 後ろの3文字
expr "string : "...\(.*\)" # 初めの3文字を取り去った部分
expr "string : "\(.*\)..." # 後ろの3文字を取り去った部分
expr "string : "\(.*\)" # その文字列をそのまま

文字列の最初の文字だけを大文字にする

#!/bin/sh
STRING=junpeko
CHAR=`expr "$STRING" : "\(.\).*"`
echo $CHAR
REMINDER=`expr $STRING : ".\(.*\)"`
echo $REMINDER
CHAR=`echo "$CHAR" | tr [a-z] [A-Z]`
STRING=$CHAR$REMINDER
echo $STRING

1行で書くと以下となります。

#!/bin/sh
STRING=junpeko
STRING=`expr "$STRING" : "\(.\).*" | tr [a-z] [A-Z]``expr "$STRING" : ".\(.*\)"`
echo $STRING

文字列の取り扱い

IFS変数

IFSはInternal Field Separatorの略で、値としてはスペースとタブ、改行がIFS変数に セットされています。

readコマンドの場合

IFS変数の値を:に置き換えて、/etc/passwdを読み込んでいます。

#!/bin/sh
OLDIFS=$IFS
IFS=:
while read USER PASSWD UID GID GCOS REMAINDER
do
	echo "$USER $GCOS"
done < /etc/passwd
IFS=$OLDIFS

ユーザー名とユーザー情報を表示しています。

for文の場合

for i in word-list
do
  ...
done

inの後に指定した文字列を順番に変数に代入して処理します。 その文字列を区切るのはタブかスペースです。 dateコマンドで挙動を確認します。

date
2021年 3 6 土曜日 08時00分59秒 JST
for i in `date`
> do
> echo $i
> done
2021年
3月
6日
土曜日
08時01分39秒
JST

setコマンドの場合

setコマンドを--の引数で実行することで、 その後の文字列を順に位置パラメタの1番からセットします。

set -- `date`
  8.3.2  while [ $# -gt 0 ]
> do
> echo $1
> shift
> done
2021年
3月
6日
土曜日
08時08分51秒
JST

awkコマンドの場合

awkはデフォルトではホワイトスペース(スペースやタブ)を区切り文字として考え、 位置パラメタと同じように$nという形で1個1個の文字列をとらえます。

echo "abc def ghi" | awk '{print $2}'
def

,はスペースとして出力されます。

date | awk '{print $2 $3 $4, $1}'
3月6日土曜日 2021

,を表示させる場合は、"で括ります。

date | awk '{print $2 $3 $4 ", " $1}'
3月6日土曜日, 2021

awkコマンドの中の$nはシェルの位置パラメタとは異なるので、 シェルに解釈されないようにシングルクォートで囲む必要があります。

また、区切り文字はFオプションで指定できます。

cat /etc/passwd | awk -F: '{print $1 ", " $5}'

cutコマンドの場合

5番目から7番目の文字を取り出します。

echo "abc:def:ghi" | cut -c5-7
def

デリミタとして:を指定して、2番目のフィールドを取得します。

echo "abc:def:ghi" | cut -d':' -f2
def

対話的な処理

FILE=/tmp/foo
echo "cannot locate file $FILE" # 変数の展開
cannot locate file /tmp/foo
echo "Errormessage." 1>&2 # エラーメッセージとして標準エラーに書き出したい場合
Errormessage.
echo "This is the first line.
> This is the second line." # 複数行の記述
This is the first line.
This is the second line.

ヒアドキュメントからの入力を標準出力へ書き出す例です。

cat <<- EOF
heredocd then else> This is the first line.
heredocd then else> This is the second line.
heredocd then else> EOF
This is the first line.
This is the second line.

awkコマンドで位置パラメタの値を任意の文字列の間に表示しています。

echo "abc" | awk '{printf("xxx%syyy\n", $1)}'
xxxabcyyy
ls -l
total 8
-rw-r--r--  1 jun  staff    0  3  6 08:50 aaa
-rwxr-xr-x  1 jun  staff  115  3  6 08:50 file_size
cat ./file_size
#!/bin/sh
for FILE in *
do
	SIZE=`wc -c < $FILE`
	echo $SIZE $FILE | awk '{printf("%5s Bytes %s\n", $1, $2)}'
done
./file_size
    0 Bytes aaa
  115 Bytes file_size

%5sという書式は5桁分の枠をとって右づめにする指定です。 カレントディレクトリのファイルサイズを出力しています。

問い合わせメッセージの出力

echoコマンドで-nオプションを指定すると、echoの後に改行しません。

ただし、環境によって挙動が異なるので注意が必要です。

例えばMacOSのbashでは想定通り動作しましたがzshshでは改行されません。

bash-3.2$ echo -n "Would you like to .... [y/n]? "
Would you like to .... [y/n]? bash-3.2$   # 改行されない

問い合わせに対する応えの取得

echo -n "Would you like to ... [y/n]? "
read ANSWER
case "$ANSWER" in
  y | yes ) FLAG=TRUE ;;
  * ) FLAG=FALSE ;;
esac
echo $FLAG

1文字だけを読み取る

#!/bin/bash
echo -n "Would you like to ... [y/n]? "
stty raw
ANSWER=`dd bs=1 count=1 2> /dev/null`
stty -raw
echo ""
case "$ANSWER" in
  [yY] ) FLAG=TRUE ;;
  * ) FLAG=FALSE ;;
esac
echo $FLAG

sttyコマンドでrawモードにすると入出力のバッファ処理を行わず、 入力したデータがそのまま次のプロセスに渡っていきます。

ddコマンドは標準入力を標準ん出力にそのままの状態でコピーするコマンドです。

入出力のブロックサイズを1(bs=1)、 入力のうち1ブロックだけを出力(count=1)ようにしています。

ddコマンドで出力されるメッセージはゴミ箱に捨てます。

その後、sttyのモードを元に戻しています。

ある条件下でのメッセージ出力

#!/bin/sh
if [ "$1" = "-v" ]; then
	VERBOSE=TRUE
	shift
fi
if [ "$VERBOSE" = "TRUE" ]; then
	echo "Message"
fi

上記のようにすると何かメッセージを出力するときは必ずif文を書かなくてはなりません。

#!/bin/sh
if [ "$1" = "-v" ]; then
	VERBOSE=TRUE
	shift
fi
if [ "$VERBOSE" = "TRUE" ]; then
	ECHO=echo
else
	ECHO=:
fi
$ECHO "Message"

上記のように、スクリプトの最初にechoコマンドのオプションがない場合に、 ヌルに置き換えてしまうとif文が不要になります。

端末画面のクリア

tput clear

最近のOSでは単純にclearでも動作します。

clear

ベルを鳴らす

OSによって異なります。

tput bel
echo '\007\c'

画面エコーバックのオンオフ

stty -echoと設定するとエコーバックがなくなり、 入力した文字は画面に表示されません。

#!/bin/sh
stty -echo
echo "Enter your password."
read PASSWORD
stty echo

プロセスの操作

UNIX上で動作しているものは全てプロセスと言います。

プロセスには一意のIDが割り当てられており、 OSha実際にはこの番号で処理しています。

psコマンドは現在動作しているプロセスの一覧を表示するコマンドです。

ps -ax | grep php | grep -v grep | awk '{print $1}'

grep検索をしたときに、実際にその名前で動作しているものと、このgrep自身の 引数として

ファイルとディレクトリ

basename

basenameコマンドはパスの最後にあるファイルの名前を出力します。

basename /etc/hosts
hosts

ディレクトリの場合も同じです。

basename /etc
etc

dirname

指定したファイルのあるディレクトリ部分だけを取り出す場合はdirnameコマンド を使います。

dirname /etc/hosts
/etc
dirname /etc
/

完全パスを得る

CURRENT=`pwd`
cd ~/Downloads
... # 何か実行
... # 何か実行
cd $CURRENT

ファイルの完全パスを得るには、basenameコマンドと組み合わせます。

FULLNAME=`pwd`/`basename echo_back_on_off`

ファイルのリスト出力

findコマンドで、directory下のファイルやディレクトリを全て表示します。

find directory -print

指定したディレクトリ下のファイルを表示します。

find . -print

完全パスで出力する場合は以下となります。

find `pwd` -print

-type dオプションでディレクトリのみ表示します。

find . -type d -print

-type fオプションでファイルのみ表示します。

find . -type f -print

-nameオプションで指定した文字列と合致するものだけ表示します。

find . -name "aaa" -print

特殊文字も利用でき、"で囲みます。

find . -name "a*" -print

また!を付けることで、否定ができます。

find . -type f ! -name "a*" -print

ディレクトリのコピー

cd SourceDirectory
cp -r . DestinationDirectory

ファイルの日付による操作

ファイルを新しい日付順に表示するには、ls -tとオプションを指定します。

もっとも新しいファイルを1つ表示するには、

ls -t aaa bbb ccc | sed -n '1p'

とします。

findコマンドに-newerというオプションがあります。

bbbというファイルがaaaというファイルより新しい場合の挙動は以下となります。

find aaa -newer bbb -print # 何も出力されない
find bbb -newer aaa -print # ファイル名が出力される
bbb

ファイルのサイズ(大きさ)の調べ方

wc -c file | awk '{print $1}'

wc-cオプションを付けるとそのファイルのバイト数を表示します。

一緒にファイル名も表示するので、awkコマンドで1番目のフィールドを取り出します。

MacOSでMacintoshHDの容量を取得する場合は、以下となります。

df | sed -n '7p' | awk '{print $5}'

OSごとのdfファイルの出力が異なるため、環境ごとにsedawkのコマンドについては 考慮が必要です。