rcmdnk's blog

7インチ カーブシザー トリミングシザー 下向き&上向き兼用 丸い先端 曲がった刃 高級鍛造仕上 ペットハサミ 犬 猫 トリミング ペット用シザー 鋏 (革ケース付き)…

シェルスクリプトの中で文字列の前後のスペースを削除する方法について。

よくあるケース

コマンドの出力の数字を使いたいけどその出力がきれいに見せるためにスペースを入れてくれてる場合など。

BSDのwcなんかで

$ n=$("a b c|wc -w)

とかするとnには 3が入ります。

また、設定ファイルとかを読み込むようにする場合、 先頭のスペースや行末のスペースは無視して良いこともあるかと思います。

echoを使った削除

よくやるのがechoを使う方法。

クォートせずにechoすると前後のスペース部分は削除されます。

$ x="  abc  "
$ echo "X${x}X"
X  abc  X
$ y=$(echo $x)
$ echo "X${y}X"
XabcX

ただし、この方法だと間にスペースがある場合、それらがすべて1つに集約されます。

$ x="  a b  c   "
$ echo "X${x}X"
X  a b  c   X
$ y=$(echo $x)
$ echo "X${y}X"
Xa b cX

もし文字列の間のスペースの数にも意味があるとするとこれだと困ります。

一方で、もし、いくつかのスペース区切りの変数を取りたいが、間のスペースはいくつあっても一区切りとみなす、という感じであればむしろ便利に使える場面もあるかもしれません。

また、この場合改行も消えます。

$ x=" a b
> c
> "
$ echo "X${x}X"
X a b
c
X
$ y=$(echo $x)
$ echo "X${y}X"
Xa b cX

いずれにしろ、echoを使った方法は前後のスペースを削除するだけではなく、間のスペースにも影響がある、ということを知っておかないといけません。

また、全角スペースがある場合には残ります。

$ x="   a b  c    "
$ echo "X${x}X"
X   a b  c    X
$ y=$(echo $x)
$ echo "X${y}X"
X a b c X

ちなみにこのechoを使った方法は

$ y=$(xargs <<< $x)

と、xargsに渡した場合でも同じ動作になります。

sedを使った方法

sedで正規表現を使えば先頭の空白と末尾の空白のみを消すことができます。

$ x="  a b  c   "
$ echo "X${x}X"
X  a b  c   X
$ y=$(sed -r 's/^[[:space:]]*|[[:space:]]*$//g' <<< $x) # For GNU sed
$ echo "X${y}X"
Xa b  cX

^[[:space:]]*(括弧は二重)で先頭から0文字以上のスペースの連続、

これで間の空白はそのままに外側の空白だけ全部削除できました。

GNUのsedであれば

$ y=$(sed 's/^[[:space:]]*\|[[:space:]]*$//g' <<< $x) # For GNU sed

でもOK。

macOSなどBSD系であれば、

$ y=$(sed -E 's/^[[:space:]]*|[[:space:]]*$//g' <<< $x) # For BSD sed

-Eオプションを使う必要があります。

この辺、sedを使う際にはGNU/BSDの違いを意識しないといけないのでちょっと面倒です。

また、上では[[:space:]]を使っていますが、これであれば全角のスペースも削除されます。

` `(半角スペースをそのまま書いたもの)を使うと全角は削除されません。

$ x="   a b  c    "
$ echo "X${x}X"
X   a b  c    X
$ y=$(sed -r 's/^[[:space:]]*|[[:space:]]*$//g' <<< $x)
$ echo "X${y}X"
Xa b  cX
$ z=$(sed -r 's/^ *| *$//g' <<< $x)
$ echo "X${z}X"
X a b  c X

なので、意図的に全角スペースを入れて残す必要がある場合を除いて[[:space:]]を使ったほうが確実です。

見た目もスペースだとぱっと見、空いているのかどうか、も分かりづらかったりもするので[[:space:]]にしておいた方がわかりやすいです。

コマンドラインでちょっとやる際には基本的に全角スペースは無いと思えばスペースを使ったほうが楽です。

また、このsedの置換では改行は置換されません。

awkを使った方法

awkでもsedと似たような感じで正規表現を使って削除できます。

$ x="  a b  c   "
$ echo "X${x}X"
X  a b  c   X
$ y=$(awk '{gsub(/^[[:space:]]+|[[:space:]]+$/,"")}1' <<< $x)
$ echo "X${y}X"
Xa b  cX

ここでも[[:space:]]を使えば全角も含めて削除できます。

また、awkもGNU/BSD版などがありますが、これに関しては同じように使えます。

このawkも改行は置換しません。

改行をsedawkで置換しようと思うと結構面倒です。

trを使った方法

trは文字を削除することが出来ますが、該当するものすべて削除するので、文字列間のスペースも削除されます。

$ x="   a b  c    "
$ echo "X${x}X"
X   a b  c    X
$ y=$(tr -d '[:space:] ' <<< $x)
$ echo "X${y}X"
XabcX

-dオプションで指定した文字列に含まれる文字をすべて削除するので、ここでは通常の半角スペースと全角スペース(クオート内の後ろの部分)を同時に指定して削除しています。

スペースに関しては[:space:](ここでは括弧は一重)も使えますがtrの場合は全角はこれでは削除できません。

tr[:space:]all horizontal or vertical whitespaceにあたるということで、このvertical whitespaceは基本的には改行にあたると考えて良くて、実際このコマンドだと改行も削除されます。

$ y=$(tr -d '  ' <<< $x)

のように通常のスペースを使って書くことも出来ますが、これだと改行は残ります。

trの方法は短く書けるので、間にスペースが絶対にこない、という場合には良いかもしれません。

特に半角スペースや改行がないなら

$ y=$(tr -d ' ' <<< $x)

で済むので。

シェルの変数展開を使う

シェルの変数展開で一部を削除したりすることが出来るのでそれを使います。

$ x="  a b  c   "
$ echo "X${x}X"
X  a b  c   X
$ y="${x#"${x%%[![:space:]]*}"}"
$ y="${y%"${y##*[![:space:]]}"}"
$ echo "X${y}X"
Xa b  cX

ここではまず、${x%%...}という展開方法を使って、後方から最長一致で該当する部分を削除する作業をしています。

[![:space:]](!が2重の括弧野中)がスペース以外を表すので、スペース以外の文字が出てからその後*で何でも、になります。

シェルの正規表現では*は直前の文字の繰り返し、ではなく、0文字以上の任意の文字列に該当するので、こうすることで何らかのスペース以外の文字が出た部分からあと全部、になります。 (ここではa b c )

で、その部分を削除したものになるので、${x%%[![:space:]]*} (先頭の2つのスペース、になります。

その上で、${x#...}で、前方から最小一致で該当する部分を削除する作業をします。 中身が先頭のスペースなので、結果的に${x#"${x%%[![:space:]]*}"}全体は先頭の連続したスペースを除いたものになります。

同様に、逆のことをやって${y%"${y##*[![:space:]]}"}では後ろの連続したスペースを除いたものになります。

2段階が必要なのとそれぞれ展開を2段階やってるのでちょっと複雑になってしまいますが、 このやり方だと全角スペースも改行も共に消えます。 逆に文字列中のスペースはそのまま残ります。

[:space:]の部分を通常の半角スペースにすれば全角スペースや改行を消さないものになります。

これであればシェルの機能だけで行えるので、もし大量にこの作業を行うとするとsedなどを使う場合に比べて大分速くなるというメリットもあり、一番きちんと使えると思ってます。

Bashのglobを使う

上のシェルの変数展開はPOSIX準拠な手法になっています。

一方で、Bashの拡張されたglobを使うとより簡潔に書くことが出来ます。

これを使うためにはextglobを有効にする必要があります。

確認するには

$ shopt extglob
extglob         on

となっていれば有効になっています。

これが

$ shopt extglob
extglob         on

となっている場合には

$ shopt -s extglob

としてextglobを有効にして上げる必要があります。

無効にするのは

$ shopt -u extglob

コマンドラインではおそらく有効になっている場合が多いかと思いますが、 シェルスクリプトを実行する際には無効になっている事があるので、 使うのであれば必ずshoptコマンドで有効する必要があります。

有効だとして、やり方は以下のような感じ。

$ x="  a b  c   "
$ echo "X${x}X"
X  a b  c   X
$ y=${x##+([[:space:]])}
$ y=${y%%+([[:space:]])}
$ echo "X${y}X"
Xa b  cX

ここではspaceの周りの括弧は二重です。

シェル展開同様全角スペースや改行も消えます。 文字列中のスペースはそのままキープ。

[[:space:]]の部分を通常のスペースにすれば半角スペースだけが削除されます。

もしextglobの状態を保ったままやりたいときは、

1
2
3
4
5
6
7
8
9
10
set_extglob=1
shopt extglob >/dev/null && set_extglob=0
if ((set_extglob));then
  shopt -s extglob
fi
y=${x##+([[:space:]])}
y=${y%%+([[:space:]])}
if ((set_extglob));then
  shopt -u extglob
fi

みたいな感じで、extglobが無効な時だけ(set_extglob=1のまま) 削除前に有効にして、削除後に戻すようにします。

実行時間

以下、実行時間の比較。 echotrと、その他のsedglobなどは結果は違うものにはなりますが、 とりあえず上でやったもので実行時間を見てみます。

環境は

  • WSL バージョン: 1.0.3.0
  • OS: Ubuntu 18.04.6 LTS
  • Bash: 5.2.15
test.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/env bash

x="   abc
def
ghi
 "

n=10000

echo "echo"
i=1
time while :;do
  y=$(echo $x)
  ((i++))
  if ((i>n));then
    break
  fi
done

echo
echo ============

echo "xargs"
i=1
time while :;do
  y=$(xargs <<< $x)
  ((i++))
  if ((i>n));then
    break
  fi
done

echo
echo ============

echo "sed"
i=1
time while :;do
  y=$(sed -r 's/^[[:space:]]*|[[:space:]]*$//g' <<< $x)
  ((i++))
  if ((i>n));then
    break
  fi
done

echo
echo ============

echo "awk"
i=1
time while :;do
  y=$(awk '{gsub(/^[[:space:]]+|[[:space:]]+$/,"")}1' <<< $x)
  ((i++))
  if ((i>n));then
    break
  fi
done

echo
echo ============

echo "tr"
i=1
time while :;do
  y=$(tr -d '[:space:] ' <<< $x)
  ((i++))
  if ((i>n));then
    break
  fi
done

echo
echo ============

echo "shell"
i=1
time while :;do
  y="${x#"${x%%[![:space:]]*}"}"
  y="${y%"${y##*[![:space:]]}"}"
  ((i++))
  if ((i>n));then
    break
  fi
done

echo
echo ============

echo "extglob"
shopt -s extglob
i=1
time while :;do
  y=${x##+([[:space:]])}
  y=${y%%+([[:space:]])}
  ((i++))
  if ((i>n));then
    break
  fi
done

10,000回ずつ実行した時間の比較です。

結果は

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
echo
X abc def ghi  X

real    0m3.114s
user    0m2.640s
sys     0m0.845s

============
xargs
X abc def ghi  X

real    0m16.523s
user    0m15.344s
sys     0m1.606s

============
sed
Xabc
def
ghiX

real    0m10.863s
user    0m10.095s
sys     0m1.045s

============
awk
Xabc
def
ghiX

real    0m14.847s
user    0m13.538s
sys     0m1.714s

============
tr
XabcdefghiX

real    0m9.022s
user    0m8.471s
sys     0m1.280s

============
shell
Xabc
def
ghiX

real    0m0.250s
user    0m0.240s
sys     0m0.010s

============
extglob
Xabc
def
ghiX

real    0m1.526s
user    0m1.482s
sys     0m0.044s

ということでシェルの変数展開を使うものが圧倒的に速いです。

やはりサブシェルを呼ばずに作業が出来ると極端に速くなることがわかります。 また、extglobも少し時間がかかってます。

意外とecho が速い。

xargsが一番遅いくらいなのはちょっと予想外でした。

また、シェルの変数展開とBashのglobの拡張を使ったものに関して 100,000回に10倍増やして回してみると

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
============
shell
Xabc
def
ghiX

real    0m2.377s
user    0m2.332s
sys     0m0.045s

============
extglob
Xabc
def
ghiX

real    0m14.943s
user    0m14.848s
sys     0m0.095s

な感じで素直に10倍になった感じです。

extglobでその時の状態によって一時的に有効にして戻すようなことをする場合、 以下のようにやってみると

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env bash

x="   abc
def
ghi
 "

n=100000

echo "disable"

shopt -u extglob
i=1
set_extglob=1
time while :;do
  shopt extglob >/dev/null && set_extglob=0
  if ((set_extglob));then
    shopt -s extglob
  fi
  y=${x##+([[:space:]])}
  y=${y%%+([[:space:]])}
  if ((set_extglob));then
    shopt -u extglob
  fi
  ((i++))
  if ((i>n));then
    break
  fi
done

echo ============
echo "enable"
shopt -s extglob
i=1
set_extglob=1
time while :;do
  shopt extglob >/dev/null && set_extglob=0
  if ((set_extglob));then
    shopt -s extglob
  fi
  y=${x##+([[:space:]])}
  y=${y%%+([[:space:]])}
  if ((set_extglob));then
    shopt -u extglob
  fi
  ((i++))
  if ((i>n));then
    break
  fi
done
1
2
3
4
5
6
7
8
9
10
11
disable

real    0m16.223s
user    0m15.908s
sys     0m0.309s
============
enable

real    0m15.797s
user    0m15.535s
sys     0m0.256s

といった感じでもともと有効なenableの方が0.5秒ほど速くなってるので shoptコマンドも全く影響ないわけではないですがあまり大きな差ではありません。

また、もともと有効でチェックもしなかった場合からは0.5秒ほど遅くなっていますがこれもそれほど、な感じです。

なので、汎用性を求めるのであればこのチェックを入れたものを使うのもありです。

また、extglobもシェルの変数展開も、関数を別に用意して削除する、みたいなことも考えたいところですが、それをサブシェルで呼んでしまうと結局遅くなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env bash

x="   abc
def
ghi
 "

n=100000

function get_y {
  local x="$1"
  local y="${x#"${x%%[![:space:]]*}"}"
  y="${y%"${y##*[![:space:]]}"}"
  echo "$y"
}

echo "shell"
y=""
i=1
y=$(get_y "$x")
echo "X${y}X"
time while :;do
  y=$(get_y "$x")
  ((i++))
  if ((i>n));then
    break
  fi
done
1
2
3
4
5
6
7
8
shell
Xabc
def
ghiX

real    0m39.088s
user    0m32.810s
sys     0m10.279s

2秒ほどで済んでいたものが20倍近くかかって、sedとかよりも遅くなっています。

サブシェルを使わずに

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env bash

x="   abc
def
ghi
 "

n=100000

function get_y {
  local x="$1"
  y="${x#"${x%%[![:space:]]*}"}"
  y="${y%"${y##*[![:space:]]}"}"
}

echo "shell"
y=""
i=1
get_y "$x"
echo "X${y}X"
time while :;do
  get_y "$x"
  ((i++))
  if ((i>n));then
    break
  fi
done

といった感じにグローバル変数に代入するような感じで使えば 基本的には直接書いたのと同じことになります。

1
2
3
4
5
6
7
8
shell
Xabc
def
ghiX

real    0m3.459s
user    0m3.459s
sys     0m0.000s

引数の引き渡しとか関数の呼び出しの部分でオーバーヘッドがあるのか1秒くらい長くかかってしまいました。 ただ、サブシェルにしてechoとかprintfで文字列を返すのに比べると格段に速い状態を保てます。

まとめ

シェルスクリプトで前後のスペースを消すには、echoを使ってやるのが一番簡単ではあります。

文字列中に空白は絶対になく、1回か2回程度行うだけ、というのであれば分かりやすいのでechoでも十分かと。

一方で文字列中のスペースを変更してしまうこともあるので、用途によっては使えないこともあるかもしれません。

前後のスペースだけちゃんと消したい場合には、速度も考えると シェルの変数展開を使う方法が一番良さそうです。

extglobを使った方法はより複雑な文字列操作が出来ますが、 今回の用途であればPOSIXなシェル変数展開で十分でそちらの方が速い、ということでした。

Ref:

Sponsored Links
Sponsored Links

« ログインシェルで使っているbashのバージョンなどをちゃんと確認する シェルスクリプトでちゃんと文字列に改行を入れ込む »

}