よくあるケース
コマンドの出力の数字を使いたいけどその出力がきれいに見せるためにスペースを入れてくれてる場合など。
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
も改行は置換しません。
改行をsed
やawk
で置換しようと思うと結構面倒です。
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 |
|
みたいな感じで、extglob
が無効な時だけ(set_extglob=1
のまま)
削除前に有効にして、削除後に戻すようにします。
実行時間
以下、実行時間の比較。
echo
とtr
と、その他のsed
やglob
などは結果は違うものにはなりますが、
とりあえず上でやったもので実行時間を見てみます。
環境は
- WSL バージョン: 1.0.3.0
- OS: Ubuntu 18.04.6 LTS
- Bash: 5.2.15
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 |
|
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 |
|
ということでシェルの変数展開を使うものが圧倒的に速いです。
やはりサブシェルを呼ばずに作業が出来ると極端に速くなることがわかります。 また、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 |
|
な感じで素直に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 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
といった感じでもともと有効な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 |
|
1 2 3 4 5 6 7 8 |
|
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 |
|
といった感じにグローバル変数に代入するような感じで使えば 基本的には直接書いたのと同じことになります。
1 2 3 4 5 6 7 8 |
|
引数の引き渡しとか関数の呼び出しの部分でオーバーヘッドがあるのか1秒くらい長くかかってしまいました。
ただ、サブシェルにしてecho
とかprintf
で文字列を返すのに比べると格段に速い状態を保てます。
まとめ
シェルスクリプトで前後のスペースを消すには、echo
を使ってやるのが一番簡単ではあります。
文字列中に空白は絶対になく、1回か2回程度行うだけ、というのであれば分かりやすいのでecho
でも十分かと。
一方で文字列中のスペースを変更してしまうこともあるので、用途によっては使えないこともあるかもしれません。
前後のスペースだけちゃんと消したい場合には、速度も考えると シェルの変数展開を使う方法が一番良さそうです。
extglobを使った方法はより複雑な文字列操作が出来ますが、 今回の用途であればPOSIXなシェル変数展開で十分でそちらの方が速い、ということでした。
Ref: