rcmdnk's blog

尊徳を発掘する―埋められたゼロからの社会構築論

シェルスクリプトとかでファイル名やディレクトリ名に数字をつける際、 桁を合わせるために上の桁を0で埋めたりすることはよくあると思いますが、 そういった数字を扱うときの注意について。

Sponsored Links

0埋め

日付とかをファイル名に付ける際、桁数を合わせるために

file_name=${year}$(printf "%02d" ${month})$(printf "%02d" ${day})

みたいにすると、year=2019month=9day=8だとすると

file_name=20190908

となります。"%02d"dが整数値で02で2文字分確保、足りない分は左側を0で埋める、という意味になります。 %2dだと足りない分は空白になります。 dの代わりにsなら通常の文字列を扱うようになります。

0始まりの文字の8進法扱い

シェルスクリプトでは変数としては全て単なる文字列で、 printfなどではそのコマンドの中で整数などを判断しますが、 整数でも8進法や16進法を認識してくれるものもあります。

printfとかだと、

  • 0で始まる数字: 8進法
  • 0xで始まる数字: 16進法

と判断されます。

$ printf "%d\n" 10
10
$ printf "%d\n" 010
8
$ printf "%d\n" 0x10
16

この時、09などは8進法でありえない数字なのでエラーになり、00を返します。

$ printf "%02d" 09
bash: printf: 09: invalid number
00

単にaとか文字列を渡した場合も同様のエラーが出ます。

逆に、printfの出力整形で、dは10進法の出力を表していて、oだと8進法、xで16進法を表示することも出来ます。

$ printf "%d\n" 10
10
$ printf "%o\n" 10
12
$ printf "%x\n" 10
a

これ以外にも算術式でも0/0xが使えます。

$ echo $((010+0x10))
24

ただし、exprbcでは通常0がついてても0を除いた整数値として扱うので注意。

$ expr 010 + 010
20
$ expr 010 + 0x10
expr: not a decimal number: '0x10'
$ echo 010 + 010|bc
20
$ echo 010 + 0x10|bc
(standard_in) 1: parse error

bcではibase=8とかすると入力を8進法にすることができますが、この場合は0などを付けずに入力します。

$ echo "ibase=8;010 + 010"|bc
16

ibase=16なら16進法でこの場合も入力数字に0xとかを付けずに。 (付けるとエラーになります。)

逆に出力を16進法とかに変えたければobase=16とかを付けます。

$ echo "obase=16;5+5"|bc
A

起こりうる問題

この様な数字の扱いですが、Bashなどのシェルスクリプトでは 変数自体は単なる文字列なのできちんと確認しないとちょっと問題が起こります。

例えば月を最終的に0埋めの文字列にしたい時。

整形するには

month=$(printf "%02d" {$month})

とか変換すれば出来ます。

ただし、 月をスクリプトの引数で設定するようにしたりする時、 人によっては親切に09などと0を付けて入力してくれるかもしれません。

この場合、上の式で

bash: printf: 09: invalid number

というエラーが出てしまいます。

また、dateコマンドとかで

date +"%m"

とかすると

09

と0埋めした状態で返してくれます。

これを上の式に渡せば上の様にエラーになります。

ただ、この問題が起こるのは8月、9月だけなので それ以外の月に作ってテストして大丈夫だと思っていると 8月に入った段階でエラーになってしまいます。 また、エラーになってもbash -eとかで終了させる様な設定にしてないと 00という文字列を使って実行してしまい予期せぬ動作をするので危険です。

月の指定であれば08, 09のみが問題で、その他の数字はそのまま正しく出るので良いのですが、 3桁以上の場合とかなら012

$ printf "%03d" 012
010

となるのでこれも予期せぬ結果になってしまいます。

なのでこの辺りの0埋め数字を扱う際にはそれぞれのタイミングできちんと確認する必要があります。

対処法

0がついてる場合に外してあげてから渡せば問題がなくなります。

上の月の例であれば0が1つつくだけなので

$ month=${month#0}

とすれば0が付いてれば消されます。

もうちょっと一般的にやりたければ

$ month=$(echo $month|sed 's/^0*//')

みたいに左の0をすべて消すようにsedとかを使えばOK。

ちょっと別の話になりますが、Linux(GNU)だと

$ month=$(echo $month|sed 's/^0\+//')

とすれば0の1回以上の繰り返しに対応しますが、 Mac(BSD)だとこの正規表現は使えません。

今回の場合は0*として0の0回以上の繰り返しに対応させても同じことなので こちらにしておいたほうが移植性があがります。

これらの様に数字をきちんと処理しても良いですが、 そもそもこの例では数字を計算しているわけではないので 単に文字列として

$ printf "%02s" $month
09

とすればmonth0でも09でも09という出力になります。 12とかもそのまま12になります。

単に数字を整形したいだけ、というのであればこ方法が一番確実かと思います。

その他の言語での8進法などの扱い

Pythonだと

  • 0b始まりで2進法
  • 0o始まりで8進法
  • 0x始まりで16進法

を表します。

>>> 10
10
>>> 0b10
2
>>> 0o10
8
>>> 0x10
16

10進法を各進法で表示したければbinocthexという組み込み関数が使えます。

>>> bin(10)
'0b1010'
>>> oct(10)
'012'
>>> hex(10)
'0xa'

これらの関数で出力されるのは文字列です。(数字じゃないので注意。)

C++だと

  • 0b始まりで2進法
  • 0始まりで8進法
  • 0x始まりで16進法

を表します。

main.cxx
1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;

int main(int argc, char **argv){
  cout << 10 << endl;
  cout << 0b10 << endl;
  cout << 010 << endl;
  cout << 0x10 << endl;
  return 0;
}

10
2
8
16

となります。

Pythonと違って8進法で0o10とかするとエラーになります。

error: unable to find numeric literal operator 'operator""o10'

coutで各進法で表示したければ マニピュレーターを使います。 std::showbase00xの表示をし、std::octstd::hexで進法を指定します。 prefixが必要ないならstd::showbaseなしで。(noshowbaseで元に戻せします。)

2進法ではhexoctの様なマニピュレーターは無いのでからりにbitsetを使い変換し、 必要であれば0bなどは自分で付ける必要があります。

main.cxx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <bitset>
using namespace std;

int main(int argc, char **argv){
  cout << showbase;
  cout << 10 << endl;
  cout << oct << 10 << endl;
  cout << hex << 10 << endl;
  cout << "0b" << bitset<4>(10) << endl;
  cout << noshowbase;
  cout << oct << 10 << endl;
  cout << hex << 10 << endl;
  return 0;
}

出力は

10
012
0xa
0b1010
12
a

となります。

Sponsored Links
Sponsored Links

« RAVPower Wi-Fi SDカードリーダーRP-WD009レビュー エコバックスのお掃除ロボット:DEEBOT OZMO 901を購入した »