第27回シェル芸勉強会に行ってきたので、その復習。
今回はsedコマンドの機能について学ぼうということで、sed縛りとなっていた。 sedコマンドというと基本は置換にしか使わないが、今回はラベルだったりパターンスペース・ホールドスペースを使ったりする。 正直、今回は全然ついていけてなかった…(´・ω・`)。 ラベルやパターンスペース・ホールドスペースの存在は知っていたのだが、それらをうまく利用できなかったと思う。 sedは奥が深い…。
問題・模範解答はこちら。 使用するファイルは1個だけなのだが、以下のコマンドでダウンロードしておこう。
git clone https://github.com/ryuichiueda/ShellGeiData.git
Q1.
Q1は、echoで取得した文字列の偶数番目のみ大文字にするというもの。 後方参照を使って偶数のみを大文字にする。
echo abcdefghijklmn | sed 's/\(.\)\(.\)/\1\u\2/g'
echo abcdefghijklmn | sed -r 's/(.)(.)/\1\u\2/g' # -rで拡張正規表現対応になる
blacknon@BS-PUB-UBUNTU-01:~$ echo abcdefghijklmn | sed 's/\(.\)\(.\)/\1\u\2/g'
aBcDeFgHiJkLmN
blacknon@BS-PUB-UBUNTU-01:~$ echo abcdefghijklmn | sed -r 's/(.)(.)/\1\u\2/g'
aBcDeFgHiJkLmN
Q2.
seqで1~100までを出力し、sedでFizzBuzz(数字が3の倍数ならFizz、5の倍数ならBuzz、3×5=15の倍数ならFizzBuzzと置換する数字遊び)をしてみる、という問題。 なんの気なしに「sed FizzBuzz」で検索してみたところ、ebanさんの過去のブログが普通にヒット。 考える前に理想的な解答見ちゃったような気持ちになったが、気を取り直して解答。 sで置換を行うパターンとcで行ごと置き換えるパターン、2通りが考えられた。
seq 1 100 | sed -e '3~3s/^.*$/Fizz/' -e '5~5s/^.*$/Buzz/g' -e '15~15s/^.*$/FizzBuzz/g'
seq 1 100|sed -e '15~15cFizzBuzz' -e '5~5cBuzz' -e '3~3cFizz' # 置き換え版
sedでは、実行コマンド(sとかcとか)の前に'first~step'を記述することで、指定された行ごとに処理が行われるようにできる。 例えば「15~15」だと15行ごとに処理が行われるので、それを利用してやるとFizzBuzzが行える。
Q3.
seqで1~10まで出力して、3行目を7行目に代入する、という問題。
ここでホールドスペース・パターンスペースの話が出てくる。 これについては、@gin_135さんの記述したこちらが詳しいと思う。 (実は見たときにホールドスペース・パターンスペースなしで解けないかと思ったのだが、どうあがいても使ったほうが短くかける)
seq 1 10 | sed '3h;3d;7G'
seq 1 10 | sed '3{h;d};7G' # 「3h;3d;」もこう記述して省略できるらしい…
blacknon@BS-PUB-UBUNTU-01:~$ seq 1 10 | sed '3h;3d;7G'
1
2
4
5
6
7
3
8
9
10
blacknon@BS-PUB-UBUNTU-01:~$ seq 1 10 | sed '3{h;d};7G' # 「3h;3d;」もこう記述して省略できるらしい…
1
2
4
5
6
7
3
8
9
10
それぞれの記述だが、以下のような意味がある。
- 3h ... 3行目をパターンスペース(あまり意識しないが、いつも使ってるとこ)からホールドスペースにコピーする
- 3d … 3行目をパターンスペースから削除する
- 7G ... ホールドスペースに入ってる内容を7行目に追記する(7gだと7行目を入れ替える)
イメージ的には、クリップボードに近いだろうか。
Q4.
C++のコードについて、関数の位置を入れ替えてコンパイル、実行するという問題(たまにあるな、このコンパイルするって問題)。 関数自体の位置を入れ替える必要があるので、複数行に対して位置の差し替えをする必要がある。 模範解答より。
cat aho.cc | sed '/ main(/,/^}/H;/ main(/,/^}/d;$G'
cat aho.cc | sed '/ main(/,/^}/H;/ main(/,/^}/d;$G' | g++ -x c++ - && ./a.out # そのままコンパイルして実行するパターン
blacknon@BS-PUB-UBUNTU-01:~/ShellGeiData/vol.27$ cat aho.cc
#include
using namespace std;
int main(int argc, char const* argv[])
{
aho();
return 0;
}
void aho(void)
{
cout << "aho" << endl;
}
blacknon@BS-PUB-UBUNTU-01:~/ShellGeiData/vol.27$ cat aho.cc | sed '/ main(/,/^}/H;/ main(/,/^}/d;$G'
#include
using namespace std;
void aho(void)
{
cout << "aho" << endl;
}
int main(int argc, char const* argv[])
{
aho();
return 0;
}
で、「なにやってるのコレ?」っていうのが、↑の状態だと正直よくわからないので分解。 記号だらけなのでぱっと見だと分からないのだが、分解してしまえば結構単純だ。
cat aho.cc | \
sed '/main(/,/^}/H; # sedでは「/開始位置/,/終了位置/」 で、対象範囲を指定できるので、それを使って「main(」~「^}」までをホールドスペースにコピー(^で行頭を意味する)
/main(/,/^}/d; # 「main(」~「^}」までを削除
$G' # $(最終行)にホールドスペースの内容を追記
Q5.
「seq 1 10 |」から始めて、奇数行と偶数行を入れ替えるという問題。 ここでもパターンスペース・ホールドスペースが用いられる。
seq 1 10 | sed '1~2{h;d};G'
blacknon@BS-PUB-UBUNTU-01:~/ShellGeiData/vol.27$ seq 1 10 | sed '1~2{h;d};G'
2
1
4
3
6
5
8
7
10
9
Q6.
「echo 1」からはじめて、行の文字数が10になるまで1を増加させていくという内容。 ラベルを用いるのだが、よくコードゴルフだと"a"とかで一文字になってる(一応以下でも"A"というラベルを用いている)。
echo 1 | sed -r ':A;p;s/./&&/;/.{10}/!b A'
blacknon@BS-PUB-UBUNTU-01:~/ShellGeiData/vol.27$ echo 1 | sed -r ':A;p;s/./&&/;/.{10}/!b A'
1
11
111
1111
11111
111111
1111111
11111111
111111111
1111111111
分解した内容は以下。
echo 1 | \
sed -r '# Aラベル開始(宣言)
:A;
p;
s/./&&/; # 条件に一致した文字を増やす(1個のみ)
# Aラベルの処理終了
/.{10}/!b A' # 文字数が10個ではない場合、Aラベルに戻る
Q7.
aというファイルを作って、それをコピーしてa1~a10というファイルを作成するという問題。 ただし、
- 使うコマンドはseq、cp、sedだけ
- ワンライナー中で数字を使わない
という縛りあり(両立しなくてもいいらしい)。 で、ここでかなり刺激があった解答が以下。
Q7再回答。今度は数字を使っていないぞ...!!
seq inf | sed '/../q' | sed 's/.*/cp a a&/e' #シェル芸— ginjiro (@gin_135) 2017年2月11日
上記回答には、個人的な学びポイントが色々と組み込まれていた。
- seqでは、引数にinfを指定することで無限に数が出せる(manに載ってないのだが、裏オプションらしい。マジか…)
- sedのqで二桁になった瞬間に処理を止める
- ~/eでOSのコマンドを実行させられる
aというファイルに関しては、/dev/nullからコピーして生成してやればcpコマンドしか使わない。
cp /dev/null ./a &&seq inf | sed '/../q' | sed 's/.*/cp a a&/e'
blacknon@BS-PUB-UBUNTU-01:/tmp/test$ ls
blacknon@BS-PUB-UBUNTU-01:/tmp/test$ cp /dev/null ./a &&seq inf | sed '/../q' | sed 's/.*/cp a a&/e'
blacknon@BS-PUB-UBUNTU-01:/tmp/test$ ls
a a1 a10 a2 a3 a4 a5 a6 a7 a8 a9
Q8.
Q6の内容について、さらに今度は逆順の出力を得るという問題(今度は10個じゃなくて5個だけど)。 tacが使えると楽なのだが、実はsedだけでtacのような処理も行えるらしい。なので、それを使ってやるとよいようだ。
echo 1 | sed -r ':A;p;s/./&&/;/.{5}/!b A' | sed 'p;1!G;h;$!d'
blacknon@BS-PUB-UBUNTU-01:~$ echo 1 | sed -r ':A;p;s/./&&/;/.{5}/!b A' | sed 'p;1!G;h;$!d'
1
11
111
1111
11111
11111
1111
111
11
1
今回は全体的に難しい…というより、sedのパターンスペース・ホールドスペースとラベルを理解しているかどうかで明暗が分かれた気がする。 普段使いだと触ってなかったのだが、今後はこの辺の機能も触っておこうかな。