かなり前回の投稿から時間が空いてしまった。夏休みなのもあり,実家に帰省したり旅行をしたりバイトがあったりなどで忙しくて記事を書ける状況ではなかったが,時間がやっとできたので前回の続きである。今回はcat,grep,sedに視点を当てたいと思う。
1. catとは?
catは concatenateという結合する・連結するを意味する英単語の省略されたコマンド名らしい。使い方としては,ファイルの中身を出力したい時に使うものである。例えば,「Hello!」と出力するためのmain.cppというC++のソースファイルの中身を見たいとしよう。以下のように実行することでmain.cppの中身を出力することができる。
$ cat main.cpp
#include <iostream>
using namespace std;
int main(void){
cout << "Hello!" << endl;
return 0;
}
さて,簡単な例を出したところで詳しくこのコマンドの使い方を深掘りしてみよう。まずは文法から見てみよう。
$ cat [オプション] [ファイル1] [ファイル2] ・・・ [ファイルn]
オプション
-n,--number・・・行番号を付ける。
-b,--number-noblank・・・空行以外に行番号を付ける。-nより優先される。
-E,--show-ends・・・行の最後に $ を付ける。
-s,--squeeze-blank・・・連続した空行の出力を行わない。分かりやすく言うと空行の間隔を1行にする。
-v,--show-noprinting・・・^ や M- 表記を使用する(LFD と TAB は除く)。
-T,--show-tabs・・・TAB文字を^Iで表示する。
-A,--show-all・・・-vETと同じ。
-e・・・-vEと同じ。
-t・・・-vTと同じ
恐らくよく使うオプションになるのは-nや-bあたりなのではないだろうか。私はcatで出力した内容を>を使って別のファイルに書き込んだりする時くらいにしかあまり使わないので,わざわざオプションを使ってコンソール画面に出力しようと思ったことはない。
1.1 -bと-nの違い
上のオプションの説明で書いた通りではあるが,実際に出力結果が異なるかを確かめてみよう。PythonでIPアドレスを取得するプログラムソースを書いたソースファイルの出力結果の差を今回は見てみる。ソースファイルは以下の通りである。
import requests
import json
def get_global_ip():
response = requests.get('https://api.ipify.org?format=json')
response.json()
ip_data = response.json()
return ip_data['ip']
global_ip = get_global_ip()
print(f"Global IP: {global_ip}")
def get_global_ipv6():
response = requests.get('https://api64.ipify.org?format=json')
response.json()
ip_data = response.json()
return ip_data['ip']
global_ipv6 = get_global_ipv6()
print(f"Global IPv6: {global_ipv6}")
requestsライブラリを利用してグローバルIPv4とGUA(Global Unicast Address)をjsonで取得して表示するプログラムである。では実際に-nと-bの差を見てみよう。
$ cat -n test-file.py
1 import requests
2 import json
3
4 def get_global_ip():
5 response = requests.get('https://api.ipify.org?format=json')
6 response.json()
7 ip_data = response.json()
8 return ip_data['ip']
9
10 global_ip = get_global_ip()
11 print(f"Global IP: {global_ip}")
12
13
14
15 def get_global_ipv6():
16 response = requests.get('https://api64.ipify.org?format=json')
17 response.json()
18 ip_data = response.json()
19 return ip_data['ip']
20 global_ipv6 = get_global_ipv6()
21 print(f"Global IPv6: {global_ipv6}")
このように空行も含めて全ての欄に行番号を付与する。
$ cat -b test-file.py
1 import requests
2 import json
3 def get_global_ip():
4 response = requests.get('https://api.ipify.org?format=json')
5 response.json()
6 ip_data = response.json()
7 return ip_data['ip']
8 global_ip = get_global_ip()
9 print(f"Global IP: {global_ip}")
10 def get_global_ipv6():
11 response = requests.get('https://api64.ipify.org?format=json')
12 response.json()
13 ip_data = response.json()
14 return ip_data['ip']
15 global_ipv6 = get_global_ipv6()
16 print(f"Global IPv6: {global_ipv6}")
結果はこのようになった。実行してみると空行だけに行番号を付与しないようだ。では最後に-bと-nを同時に利用するとどうなるかを確かめてみよう。manページ通りなら-bの実行結果と同じになるはずである。
$ cat -b -n test-file.py
1 import requests
2 import json
3 def get_global_ip():
4 response = requests.get('https://api.ipify.org?format=json')
5 response.json()
6 ip_data = response.json()
7 return ip_data['ip']
8 global_ip = get_global_ip()
9 print(f"Global IP: {global_ip}")
10 def get_global_ipv6():
11 response = requests.get('https://api64.ipify.org?format=json')
12 response.json()
13 ip_data = response.json()
14 return ip_data['ip']
15 global_ipv6 = get_global_ipv6()
16 print(f"Global IPv6: {global_ipv6}")
このように出力されたのでmanページ通り,-bの方が-nより優先されることが分かった。
1.2 macOS標準のcatの場合
仕様が若干異なるようで,オプションで指定する際は全て小文字で指定する必要があるようだ。大文字を使ってGNU/Linux版のcatと同じオプションを使用するとcat: illegal optionと表示されてしまった。そもそも--という様にすること自体もダメなようだ。macOS版のcatに用意されているオプションは以下の通りだ。英語の原文そのままで貼り付ける。
DESCRIPTION
The cat utility reads files sequentially, writing them to the standard output. The file operands are processed in command-line order. If file is a single dash (‘-’) or absent, cat reads from the standard input. If file
is a UNIX domain socket, cat connects to it and then reads it until EOF. This complements the UNIX domain binding capability available in inetd(8).
The options are as follows:
-b Number the non-blank output lines, starting at 1.
-e Display non-printing characters (see the -v option), and display a dollar sign (‘$’) at the end of each line.
-l Set an exclusive advisory lock on the standard output file descriptor. This lock is set using fcntl(2) with the F_SETLKW command. If the output file is already locked, cat will block until the lock is acquired.
-n Number the output lines, starting at 1.
-s Squeeze multiple adjacent empty lines, causing the output to be single spaced.
-t Display non-printing characters (see the -v option), and display tab characters as ‘^I’.
-u Disable output buffering.
-v Display non-printing characters so they are visible. Control characters print as ‘^X’ for control-X; the delete character (octal 0177) prints as ‘^?’. Non-ASCII characters (with the high bit set) are printed as
‘M-’ (for meta) followed by the character for the low 7 bits.
-aや-AなどのAll系のオプション自体が存在しない。なので,-Aのように指定したければ-vetと指定する必要があるということだ。厄介な仕様の違いである。
cat以外にもLinuxとmacOSでは仕様の違うコマンドはいくつか存在する。有名でよく使われるコマンドであげるとなるとpingとtracerouteが代表例だろう。macOSの場合はIPv4専用とIPv6専用で別々のバイナリファイルが用意されており,Linuxと違ってシンボリックリンクというわけではないのでpingやtracerouteではIPv4しか扱えない。IPv6を扱う場合はping6やtraceroute6のようにする必要がある。Linuxのping6とtraceroute6は/usr/bin内のpingとtracerouteへのシンボリックリンクになっているだけなので,pingとtracerouteのバイナリファイルはIPv4とIPv6の両方を扱うことができる。なので,-4や-6の様にIPのバージョンを指定できるオプションが存在するのである。
2. grepとは?
grepは Global Regular Expression Print の略である。grepは正規表現に一致する行を表示するコマンドである。色々なコマンドと組み合わせて使用するコマンドであり,|(パイプ)を用いてgrepで情報を絞るのが一般的である。例として簡単な使い方を紹介する。iproute2パッケージに存在するipコマンドでIPアドレスを表示させることができる。IPアドレスだけを表示したい時,grepでinetの部分だけを出力するようにするとIPアドレスだけが表示されるようになり見やすくなる。
$ ip a s | grep inet
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host noprefixroute
inet 192.168.1.23/24 brd 192.168.1.255 scope global dynamic noprefixroute eno1
inet6 240b:xx:xx:xx:yy:yy:yy:yy/64 scope global dynamic noprefixroute
inet6 fe80::yy:yy:yy:yy/64 scope link noprefixroute
このような表示にすることができる。macOS版のiproute2macは機能がクソみたいにゴミなのでinetとinet6の後はアドレスしか表示されずipコマンドのオプションであるscopeが使えないクソ仕様である。
inet 127.0.0.1/8
inet6 ::1/128
inet6 fe80::1/64
inet6 fe80::8db:9543:2f48:3626/64
inet 192.168.1.2/24 brd 192.168.1.255
inet6 240b:xx:xx:xx:yy:yy:yy:yy/64
inet6 240b:xx:xx:xx:yy:yy:yy:yy/64
inet6 240b:xx:xx:xx:yy:yy:yy:yy/64
inet6 240b:xx:xx:xx:yy:yy:yy:yy/64
inet6 fe80::yy:yy:yy:yy/64
・
・
・
inet6 fe80::yy:yy:yy:yy/64
ガチでこのような結果になるのでクソである。macOSではipコマンドではなくifconfigを使った方が良いのだ。(grepの話なのにipコマンドの話になっているんだ?)
...脱線したがgrepコマンドの良く使用されるオプションの解説をする。
よく使用されるオプション(個人的に使うものであるので間違ってるかも...?)
正規表現の選択
-E,--extended-regexp・・・パターンを拡張正規表現として扱う。egrepはgrep -Eと一緒である。
-F,--fixed-strings・・・パターンを正規表現の代わりに改行で区切られた固定文字列として扱い,その文字列のいずれかとマッチするかを調べる。fgrepはgrep -Fと同じである。(正規表現を検索するコマンドなのに正規表現を使わない様にするオプションなのでなんか名前と矛盾しているような気がする...)
-G,--basic-regexp・・・パターンを基本正規表現として扱う。これがデフォルトであり,特に指定しない限りは暗黙的に-Gが実行されているのと一緒である。
-P,--perl-regexp・・・パターンをPerl互換のある正規表現として扱う。極めて実験的なものであるので,grep -Pを使うと警告が表示される可能性がある。pgrepはgrep -Pと同じである。
パターンマッチングの制御
-v・・・指定したパターンにマッチしない行を表示しない。
-i,--ignore-case,-y・・・アルファベットの大文字と小文字を区別しない様にする。
その他のオプション
-n,--line-number・・・各出力行の前に,その入力ファイル内での1から始まる行番号を表示する。
-o,--only-matching・・・パターンにマッチした部分文字列のみを表示する。
-c,--count・・・通常の出力はせず,各入力ファイルについてマッチした行数を表示する。
-A NUM,--after-context=NUM・・・パターンにマッチした行の後の行をNUMに指定した数の行数分表示する。
-B NUM,--before-context=NUM・・・パターンにマッチした行の前の行をNUMに指定した数の行数分表示する。
-C NUM,-NUM,--context=NUM・・・パターンにマッチした行の前後の行をNUMに指定した数の行数分表示する。-A NUM -B NUMと同じ。
この辺りがよく使われると思われる。長いログを精査する時に役立つだろう。私は友人に依頼されてマイクラサーバーを運営しているのだが,たまにサーバーがクラッシュするのでそのクラッシュログを読むときにgrepで情報を吸い出して,別ファイルに出力した上でそのファイルを読んで原因分析をする時に使用したりする。もっと他にもオプションはあるが,私は使わないので他のオプションについては是非manページを読むなどして調べてみると良いだろう。
3.sed とは?
sedはstream editorの略である。主な使い方は,パターンや文字列の置き換え,抽出や挿入がメインである。実際に使用例を見た方が分かりやすいだろう。では,やってみよう。
iptablesでルールを一括で更新したいとしよう。数行なら手作業でやってもそこまで時間が掛からないので問題は無いが,数十行以上あるとあまりにも苦になる。なので,RULE1とRULE2の部分をsedで一括で文字の置き換えをしてみよう。
余談だが,現在Linuxのソフトウェアファイアウォールはiptablesなどではなく,arptables・ebtables・iptables・ip6tablesが統合されたnftablesへの移行が推奨されている。これらの4つのパッケージにはnftables互換のパッケージがあるのでそちらを使いながらnftablesに移行する事も検討すると良いだろう。
$ cat /etc/iptables/rules.v4
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [45379:6445483]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
#IP制限(JP)
-A RULE1 -s 1.0.16.0/20 -j RULE2
-A RULE1 -s 1.0.64.0/18 -j RULE2
-A RULE1 -s 1.1.64.0/18 -j RULE2
-A RULE1 -s 1.5.0.0/16 -j RULE2
-A RULE1 -s 1.21.0.0/17 -j RULE2
-A RULE1 -s 1.21.128.0/18 -j RULE2
-A RULE1 -s 1.21.192.0/19 -j RULE2
-A RULE1 -s 1.33.0.0/16 -j RULE2
-A RULE1 -s 1.66.0.0/15 -j RULE2
-A RULE1 -s 1.72.0.0/13 -j RULE2
-A RULE1 -s 1.112.0.0/14 -j RULE2
-A RULE1 -s 14.0.8.0/22 -j RULE2
-A RULE1 -s 14.1.4.0/22 -j RULE2
-A RULE1 -s 14.1.8.0/21 -j RULE2
-A RULE1 -s 14.3.0.0/16 -j RULE2
-A RULE1 -s 14.8.0.0/13 -j RULE2
-A RULE1 -s 14.101.0.0/16 -j RULE2
-A RULE1 -s 14.102.132.0/22 -j RULE2
-A RULE1 -s 14.102.192.0/19 -j RULE2
-A RULE1 -s 14.128.0.0/22 -j RULE2
-A RULE1 -s 14.128.16.0/20 -j RULE2
・
・
・
COMMIT
置き換える時は以下のように実行する。今回はファイルの上書きもするので,-iオプションを使用する。
# sed -e 's/RULE1/INPUT/g' -e 's/RULE2/ACCEPT/g' -i /etc/iptables/rules.v4
これで置き換え操作が完了する。sedが何をしたかを解説する。
・s/regexp/replacement/について
regexpに指定したパターンを探す。マッチに成功した場合はregexpをreplacementに指定した文字列に置き換えをする。-eオプションを使うことで複数回置き換えをすることができる。
・-i,-eについて
-iはファイルに対して操作するとき,そのファイルに該当する部分を上書きする。s/regexp/replacement/と-eなどで指定して置き換えした部分をファイルに上書きして置き換えるのである。上の例で言うと,RULE1がINPUTに,RULE2がACCEPTに置き換えられる。
-eを複数回使うことで1回のコマンド操作で複数の置き換え操作をすることも可能だ。実際にやってみよう。
今回はnftablesの設定ファイルでIPアドレス単位でパケットフィルタリング設定をする時のIPアドレスの置換操作をする。例としてx.x.x.xを192.168.1.254に,y.y.y.yを192.168.1.0/24に置き換える。
#!/usr/sbin/nft -f
flush ruleset
table inet filter{
chain input {
type filter hook input priority 0;
policy drop;
ct state { established, related } accept
iifname "lo" accept
ip6 nexthdr icmpv6 accept
icmp type { echo-request, echo-reply } accept
icmpv6 type { echo-request, echo-reply, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept
#TCP IPv4 Rules
ip daddr x.x.x.x tcp dport { 80, 443, 8080, 8443 } accept
ip daddr x.x.x.x ip saddr y.y.y.y tcp dport { 0-65535 } accept
・
・
・
# sed -i -e "s/x.x.x.x/192.168.1.254/g" -e "s/y.y.y.y/192.168.1.0\/24/g" /etc/nftables.conf
#TCP IPv4 Rules
ip daddr 192.168.1.254 tcp dport { 80, 443, 8080, 8443 } accept
ip daddr 192.168.1.254 ip saddr 192.168.1.0/24 tcp dport { 0-65535 } accept
このように複数の置換操作を一度の操作で行うこともできる。ぜひ使いこなしたいオプションの一つだ。私のメインPCはMacなのでMac標準のsedを使ったところ,-eを複数使って一度に複数の置換操作をすることはできなかった。BSD系のコマンドをMacはベースにしているのでGNU系ベースのLinuxとは別なのだろうと考えている。他にもcoreutilsパッケージのddコマンドの挙動も異なる。GNU系の方のdd(8.22以降)はstatus=progressで進捗度合いの表示が可能だが,BSD系のddでは使えないオプションである。
話が脱線したが,他にもsedには色々なオプションが存在する。
指定した行の削除
例としてsed "2,4d" main.cppを実行してみよう。main.cppの中身は以下のようにする。
#include <iostream>
using namespace std;
int main(void){
cout << "Hello!" << endl;
return 0;
}
#include <iostream>
using namespace std;
int mian(void){
cout << "Hello!" << endl;
return0;
}
このように表示されるはずだ。上記で実行されたのは2行目と4行目の削除である。main.cppの2行目と4行目は空行なのでその部分が消されて,このような表示がされるようになる。-iをつけて実行すればファイルに書き込み処理がされるのでmain.cppの空行が消えることになる。
他にも特定の文字列を含む行だけの削除もできる。その場合は"/${特定の文字列}/d"という風に指定すれば特定の文字列を含んだ行の削除が可能である。例えばコメントアウトが多くて読みづらい場合は, sed -i "/\#/d" target.confのようにすれば#でコメントアウトされた行が全て消すことができる。
特定の行の表示
特定のm行目からn行目の部分のみを表示したいという時に使うオプションが-nである。以下のような使い方をする。
$ sed -n m,np [target.file]
では,main.cppの3~7行目の表示をしてみよう。
$ sed -n 3,7p main.cpp
using namespace std;
int main(void){
cout << "Hello!" << endl;
return 0;
このように3~7行目のみの表示がされる。個人的にはsedは-i,-eくらいしか使わないので-nはどういう場面で使うかが想像つかない。例えばnginxとかのリソースファイルでnginx -tを実行してこの行が構文エラー(Syntax Error)が出てる時とかに使うのかもしれない。
4.おわりに
catやsed,grepは非常によく使うコマンドなので使い方を覚えて損はない。例えば公開鍵認証でSSH接続する時,接続先のサーバーに公開鍵を登録するとき,直接viやnanoなどで編集しなくてもcatと>でcat pubkey.pub >> ~/.ssh/authorized_keysとやればファイルの直接編集をしなくても解決する。是非使えるようになりたいコマンドだ。grepもpsコマンドで表示した特定のプロセスを絞ったり,lsofで表示した特定のポートを利用しているプログラム名を絞ったりする時にも使える。GNU/Linuxコマンドは非常に汎用性が高く,奥深い。まだまだ勉強中で,もっと使いこなせるようになりたい。