1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GNU/Linuxコマンドについての理解を深める~その2~

Posted at

かなり前回の投稿から時間が空いてしまった。夏休みなのもあり,実家に帰省したり旅行をしたりバイトがあったりなどで忙しくて記事を書ける状況ではなかったが,時間がやっとできたので前回の続きである。今回はcat,grep,sedに視点を当てたいと思う。

1. catとは?

catconcatenateという結合する・連結するを意味する英単語の省略されたコマンド名らしい。使い方としては,ファイルの中身を出力したい時に使うものである。例えば,「Hello!」と出力するためのmain.cppというC++のソースファイルの中身を見たいとしよう。以下のように実行することでmain.cppの中身を出力することができる。

bash or zsh
$ 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- 表記を使用する(LFDTAB は除く)。
-T,--show-tabs・・・TAB文字を^Iで表示する。
-A,--show-all・・・-vETと同じ。
-e・・・-vEと同じ。
-t・・・-vTと同じ

恐らくよく使うオプションになるのは-n-bあたりなのではないだろうか。私はcatで出力した内容を>を使って別のファイルに書き込んだりする時くらいにしかあまり使わないので,わざわざオプションを使ってコンソール画面に出力しようと思ったことはない。

1.1 -b-nの違い

上のオプションの説明で書いた通りではあるが,実際に出力結果が異なるかを確かめてみよう。PythonでIPアドレスを取得するプログラムソースを書いたソースファイルの出力結果の差を今回は見てみる。ソースファイルは以下の通りである。

test-file.py
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の差を見てみよう。

-nの場合の実行結果
$ 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}")

このように空行も含めて全ての欄に行番号を付与する。

-bの場合の実行結果
$ 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に用意されているオプションは以下の通りだ。英語の原文そのままで貼り付ける。

manページ
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以外にもLinuxmacOSでは仕様の違うコマンドはいくつか存在する。有名でよく使われるコマンドであげるとなるとpingtracerouteが代表例だろう。macOSの場合はIPv4専用とIPv6専用で別々のバイナリファイルが用意されており,Linuxと違ってシンボリックリンクというわけではないのでpingtracerouteではIPv4しか扱えない。IPv6を扱う場合はping6traceroute6のようにする必要がある。Linuxping6traceroute6/usr/bin内のpingtracerouteへのシンボリックリンクになっているだけなので,pingtracerouteのバイナリファイルはIPv4とIPv6の両方を扱うことができる。なので,-4-6の様にIPのバージョンを指定できるオプションが存在するのである。

2. grepとは?

grepGlobal Regular Expression Print の略である。grepは正規表現に一致する行を表示するコマンドである。色々なコマンドと組み合わせて使用するコマンドであり,|(パイプ)を用いてgrepで情報を絞るのが一般的である。例として簡単な使い方を紹介する。iproute2パッケージに存在するipコマンドでIPアドレスを表示させることができる。IPアドレスだけを表示したい時,grepinetの部分だけを出力するようにすると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は機能がクソみたいにゴミなのでinetinet6の後はアドレスしか表示されずipコマンドのオプションであるscopeが使えないクソ仕様である。

実行結果(macOSのiproute2macの場合)
    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・・・パターンを拡張正規表現として扱う。egrepgrep -Eと一緒である。
-F,--fixed-strings・・・パターンを正規表現の代わりに改行で区切られた固定文字列として扱い,その文字列のいずれかとマッチするかを調べる。fgrepgrep -Fと同じである。(正規表現を検索するコマンドなのに正規表現を使わない様にするオプションなのでなんか名前と矛盾しているような気がする...)
-G,--basic-regexp・・・パターンを基本正規表現として扱う。これがデフォルトであり,特に指定しない限りは暗黙的に-Gが実行されているのと一緒である。
-P,--perl-regexp・・・パターンをPerl互換のある正規表現として扱う。極めて実験的なものであるので,grep -Pを使うと警告が表示される可能性がある。pgrepgrep -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 とは?

sedstream editorの略である。主な使い方は,パターンや文字列の置き換え,抽出や挿入がメインである。実際に使用例を見た方が分かりやすいだろう。では,やってみよう。

iptablesでルールを一括で更新したいとしよう。数行なら手作業でやってもそこまで時間が掛からないので問題は無いが,数十行以上あるとあまりにも苦になる。なので,RULE1RULE2の部分をsedで一括で文字の置き換えをしてみよう。

余談だが,現在Linuxのソフトウェアファイアウォールはiptablesなどではなく,arptablesebtablesiptablesip6tablesが統合されたnftablesへの移行が推奨されている。これらの4つのパッケージにはnftables互換のパッケージがあるのでそちらを使いながらnftablesに移行する事も検討すると良いだろう。

/etc/iptables/rules.v4
$ 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オプションを使用する。

/bin/zsh or /bin/bash
# sed -e 's/RULE1/INPUT/g' -e 's/RULE2/ACCEPT/g' -i /etc/iptables/rules.v4

これで置き換え操作が完了する。sedが何をしたかを解説する。

s/regexp/replacement/について

regexpに指定したパターンを探す。マッチに成功した場合はregexpreplacementに指定した文字列に置き換えをする。-eオプションを使うことで複数回置き換えをすることができる。

-i,-eについて

-iはファイルに対して操作するとき,そのファイルに該当する部分を上書きする。s/regexp/replacement/-eなどで指定して置き換えした部分をファイルに上書きして置き換えるのである。上の例で言うと,RULE1がINPUTに,RULE2がACCEPTに置き換えられる。
-eを複数回使うことで1回のコマンド操作で複数の置き換え操作をすることも可能だ。実際にやってみよう。
今回はnftablesの設定ファイルでIPアドレス単位でパケットフィルタリング設定をする時のIPアドレスの置換操作をする。例としてx.x.x.x192.168.1.254に,y.y.y.y192.168.1.0/24に置き換える。

/etc/nftables.conf
#!/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の中身は以下のようにする。

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.おわりに

catsed,grepは非常によく使うコマンドなので使い方を覚えて損はない。例えば公開鍵認証でSSH接続する時,接続先のサーバーに公開鍵を登録するとき,直接vinanoなどで編集しなくてもcat>cat pubkey.pub >> ~/.ssh/authorized_keysとやればファイルの直接編集をしなくても解決する。是非使えるようになりたいコマンドだ。greppsコマンドで表示した特定のプロセスを絞ったり,lsofで表示した特定のポートを利用しているプログラム名を絞ったりする時にも使える。GNU/Linuxコマンドは非常に汎用性が高く,奥深い。まだまだ勉強中で,もっと使いこなせるようになりたい。

前回の記事

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?