読者です 読者をやめる 読者になる 読者になる

awkスクリプトのデバック方法

ここひと月ぐらい割とawkを使うようになり、自分なりにmanを読んだり、試行錯誤したawkスクリプトのデバック方法のメモ。

環境

基本的には以下のバージョンのgawkで試している。

[mk55@localhost ~]$ awk --version | head -n 1
GNU Awk 4.0.2

gawkのデバックオプションを使う

gawk-dオプション(もしくは--dump-variables)を使うと組み込み変数を含むawkの変数の最終的な値をファイルに出力できる。

以下のようなサンプルスクリプト1_debug.awkがあったとすると

{
        baz="baz" ;
        print $1, baz ;
 }

デバックオプション-dをつけると以下のように変数の内容をダンプしたawkvars.outがカレントディレクトリに生成される

[mk55@localhost ~]$ echo "foo bar" | gawk -d -f 1_debug.awk 
foo baz

#変数のダンプファイル。
#組み込み変数とともに、最終行に定義した変数bazの値が出力されている。
[mk55@localhost ~]$ cat awkvars.out 
ARGC: 1
ARGIND: 0
ARGV: array, 1 elements
BINMODE: 0
CONVFMT: "%.6g"
ERRNO: ""
FIELDWIDTHS: ""
FILENAME: "-"
FNR: 1
FPAT: "[^[:space:]]+"
FS: " "
IGNORECASE: 0
LINT: 0
NF: 2
NR: 1
OFMT: "%.6g"
OFS: " "
ORS: "\n"
RLENGTH: 0
RS: "\n"
RSTART: 0
RT: "\n"
SUBSEP: "\034"
TEXTDOMAIN: "messages"
baz: "baz"

manにも書いてある通り、typoで変な変数を定義していたり、ループの添え字(i, j)が意図しない値になっていたりすることを検出できることがある。

なお、以下のようにファイルを指定して出力することもできる。

[mk55@localhost ~]$ echo "foo bar" | gawk -d/tmp/debug.txt -f 1_debug.awk 
foo baz
[mk55@localhost ~]$ ls /tmp/debug.txt 
/tmp/debug.txt
[mk55@localhost ~]$ head /tmp/debug.txt 
ARGC: 1
ARGIND: 0
ARGV: array, 1 elements
BINMODE: 0
CONVFMT: "%.6g"
ERRNO: ""
FIELDWIDTHS: ""
FILENAME: "-"
FNR: 1
FPAT: "[^[:space:]]+"
[mk55@localhost ~]$ 

printデバックする

AWK Users JP :: awk 基礎文法最速マスターにある通り、awkの実装によらずデバックできるのでprintデバック(もしくはprintfデバック)はよく使うと思う。 ただ、単純にprintを仕込むとデバック用途なのか、通常の用途なのかわからないので一工夫する。

以下のようなサンプルスクリプト2_debug.awkがあったとする。

{
        baz="baz" ;
        if(debug)  print "[debug] baz is :" baz  > "/dev/stderr" ;
        print $1, baz ;
 }

上のスクリプトは通常時は1_debug.awkと同じ処理をするが、変数debugが0以外で定義されている場合は標準エラー出力に対してprint文で文字列を出力する*1。 これを利用して、変数を定義できる-v(もしくは--assign)オプションを疑似的なdebug print用のオプションのように使う*2*3

#変数debugがないので通常の出力のみ
[mk55@localhost ~]$ echo "foo bar" | gawk -f  2_debug.awk 
foo baz

#変数debugが1なのでデバック出力あり
[mk55@localhost ~]$ echo "foo bar" | gawk -v debug=1 -f 2_debug.awk 
[debug] baz is :baz
foo baz

上記では変数の出力だけなのでありがたみがないが、例えばBEGINFILEENDFILE等に記載すれば、どのファイルの処理を開始/終了したか出力したりもできる。

なお、本番向けスクリプトにdebug用の記述が入るのが不快ならsedで一括置換してしまえばよい*4

[mk55@localhost ~]$ sed -i 's/if(debug)/#if(debug)/g' 2_debug.awk 
[mk55@localhost ~]$ cat 2_debug.awk 
{
        baz="baz" ;
        #if(debug)  print "[debug] baz is :" baz  > "/dev/stderr" ;
        print $1, baz ;
 }
[mk55@localhost ~]$ echo "foo bar" | gawk -v debug=1 -f 2_debug.awk 
foo baz

もし何度も上記を呼ぶような大きなスクリプトを書くなら、いっそ関数化する手もありそうではある*5

{
        baz="baz" ;
        printDebugMsg( "baz is : " baz )
        print $1, baz ;
}
function printDebugMsg(msg)
{
        if(debug) print "[debug]" msg > "/dev/stderr"
}

gawkの互換性チェックのオプションを使う

あまりないとは思うが、awkの処理を処理系の異なるawkに移植する際は、事前に--lintオプションを使って動かしておくと他の処理系で動作しなさそうな箇所を警告してくれる。 以下の例はgawkにはあり、nawkにはない関数systime()strftime()を使った処理で--lintを使ってみた場合。

[mk55@localhost ~]$ echo "now is" | gawk '{print $0 " " strftime("%Y/%m/%d", systime()) }'
now is 2017/05/28

[mk55@localhost ~]$ echo "now is" | gawk --lint '{print $0 " " strftime("%Y/%m/%d", systime()) }'
gawk: cmd. line:1: warning: `strftime' is a gawk extension
gawk: cmd. line:1: warning: `systime' is a gawk extension
now is 2017/05/28

gawk付属のプロファイラを使う

gawkにはPAWKというプロファイラがついているので、これを利用して解析することもできる。 pawkコマンドを使うか--profileもしくは-pオプションを利用すると使用できる

例えば以下のようなことができる。

  • BIGINやEND、funcitionなどを種類ごとにまとめて表示*6
  • ループ処理が何回呼ばれたか表示
  • パターンやif-else分岐が何度呼ばれたか表示

細かい利用方法は以下を読めばだいたいわかる。 The GNU Awk User’s Guide: Profiling

gawk付属のデバッガを使う

gawkにはDAWKというデバッガがついているのでこれを使うという手もある。

The GNU Awk User’s Guide: Debugging

対話的にコマンド入力したりしてブレークポイントを設定したりなどいろいろできるようだ。 ただ、私自身はデバッガが必要なほど複雑なスクリプトを作ったことはないので、いままでお世話になったことはない。 なので、正直なところ良く分からない。

日本語だとかろうじてこのページでちょっと触れられているぐらいではAWK Users JP :: 【緊急特集】最新の gawk 4.0.0 を追え!

参考

The GNU Awk User’s Guide: Indexgawkのマニュアルページ

AWK 処理系の比較 | YOSBITS ⇒nawk, gawk, mawkの比較。mawkというものがあるのは初めて知った。

Man page of STRFTIMEC言語strftime()のman。awkでも同じような動きをしているようだ。

*1:標準エラー出力に出力するのは、標準出力と区別することでawkの結果と別に出力できるようにするため。例えば、awkの結果をリダイレクトでファイルに出す場合などでも、debug printはコンソール上に表示したり、別のファイルに出力できたりする

*2:Powershellの-DebugとWrite-Debugのようなイメージ

*3:nawkでも-vオプションは利用可能

*4:個人的にはそこまで性能的に問題になることは少ないと思うので、2重に管理するぐらいなら残した方が良いと思うが

*5:awkの関数ってキャメルケースで良いのだろうか?print_debug_msgとかのほうが適切な気もするが…適切な既存の規約を見つけられなかった

*6:たとえば2つのBIGINが書かれてしまっているときの解析に便利

CentOSにおけるコマンドの格納パスのおさらい

CentOSの一般ユーザーでlsofを使おうとしたがPATHが通っていなかった。なぜだろうと調べたところ、Filesystem Hierarchy Standardで決まっているコマンドの格納パスについてすっかり忘れていたのでおさらいした際のメモ

本論

あるサーバーでlsofを使おうとしたら以下の通り存在するがPATHが通っておらず実行できなかった。

疑似的に再現すると以下のような形

#lsofが見つからず実行できない
[mk55@localhost ~]$ lsof
bash: lsof: command not found...

#$PATH以下をwhichコマンドで探してみてもない
[mk55@localhost ~]$ which lsof
/usr/bin/which: no lsof in (/usr/local/bin:/usr/bin:/usr/local/sbin:/home/mk55/.local/bin:/home/mk55/bin)

#一般的なパス+$PATH+$MANPATHを検索してくれるwhereisで探してみると/usr/sbinの中にあった
[mk55@localhost ~]$ whereis lsof
lsof: /usr/sbin/lsof /usr/share/man/man8/lsof.8.gz

で、なぜだろうと思ったのだが、そもそもコマンドの用途によって実行可能なユーザーって決まっていたような…… というわけで再度調べてみると以下の通りになっているそうだ。

PATH 内容 CentOS6以前 CentOS7以前
/bin 一般ユーザーでも使えてシングルユーザーモード*1に必須な一般的なコマンドが配置される あり なし 
/sbin rootユーザーが使える管理コマンドが配置される あり なし 
/lib /binと/sbinで使用する共有ライブラリやカーネルモジュール あり なし 
/usr/bin 一般ユーザーでも使えてシングルユーザーモードに必須でない一般的なコマンドが配置される あり あり
/usr/sbin 必須でない管理コマンドが配置される あり あり
/usr/lib /usr/binや/usr/sbin以下が使用する共有ライブラリやカーネルモジュール あり あり
/usr/local ローカル環境で使うものが配置される。配下にbin/, lib/がある あり あり


上の表で”なし”と書いたディレクトリは実態が存在しなくなり、ファイルは/usr配下へ統合され、フォルダは統合先へのシンボリックリンクになっている 試しにCentOS7でls -al /してみると確かにシンボリックリンクになっている。

[mk55@localhost ~]$ cat /etc/redhat-release 
CentOS Linux release 7.3.1611 (Core) 
[mk55@localhost ~]$ ls -l /
total 36
lrwxrwxrwx.   1 root root     7 May  1 18:06 bin -> usr/bin
dr-xr-xr-x.   4 root root  4096 May  6 10:27 boot
drwxr-xr-x.  19 root root  3320 May 24 23:50 dev
drwxr-xr-x. 139 root root 12288 May 25 00:02 etc
drwxr-xr-x.   3 root root    17 Nov  6  2016 home
lrwxrwxrwx.   1 root root     7 May  1 18:06 lib -> usr/lib
lrwxrwxrwx.   1 root root     9 May  1 18:06 lib64 -> usr/lib64
drwxr-xr-x.   2 root root     6 Nov  6  2016 media
drwxr-xr-x.   3 root root    18 Nov  6  2016 mnt
drwxr-xr-x.   3 root root    15 Nov  6  2016 opt
dr-xr-xr-x. 130 root root     0 May 24 23:50 proc
dr-xr-x---.  13 root root  4096 May  6 13:18 root
drwxr-xr-x.  35 root root  1120 May 25 00:02 run
lrwxrwxrwx.   1 root root     8 May  1 18:06 sbin -> usr/sbin
drwxr-xr-x.   2 root root     6 Nov  6  2016 srv
dr-xr-xr-x.  13 root root     0 May 24 23:50 sys
drwxrwxrwt.  10 root root  4096 May 25 00:02 tmp
drwxr-xr-x.  13 root root  4096 May  1 18:06 usr
drwxr-xr-x.  23 root root  4096 May 24 23:50 var

参考

  1. whereisとwhereの違い ⇒ 探し物は何ですか?(Linuxコマンドパス検索「which」他編) | UserSide Staff Blog

  2. RHEL7(CentOS7)以降のファイルシステムレイアウトの変更内容 ⇒ 2.3. ファイルシステムレイアウト

    /bin、/sbin、/lib、および /lib64 ディレクトリーは、/usr 下に移動しています。

  3. Linuxディレクトリ構造についてもっと知りたいならFilesystem Hierarchy Standardを調べましょう

*1:Wikipedia日本語版に記載がない!

Excel VBAでシートの存在有無を判定するのにループはいらない。On Errorステートメントを使おう

Excel VBAでシートの存在有無を判定する場合にループ使うのは冗長だなーと思ったのだが、実はOn Errorステートメントを使うとすっきりと書けた、という話。

元ネタはExcel VBA If WorkSheet(“wsName”) Exists - Stack Overflow

続きを読む

ワイルドカードによるマッチングはGLOBっていうんですね

日常的にワイルドカードを使っているくせにちゃんと名前を知らなかったんだなあ、というメモ。

ワイルドカードってのはls *.txtのようなやつ。

文字列に ‘?’, ‘*’, ‘[’ が含まれていると、 それはワイルドカードパターンとみなされる。 「glob する」というのは、ワイルドカードパターンを展開して、 そのパターンにマッチするパス名のリストを得ることである。

出典: Man page of GLOB

参考

Man page of GLOB

glob (programming) - Wikipedia)*1

BashのGlobは積極的に利用しましょう - 理系学生日記

glob(グロブ)について - Qiita

*1:日本語のWikipediaには記載がなかった。

CentOS7でNetworkManager(nmcli)を使用してNICを自動起動にする

自前の仮想マシン(CentOS7)にsshしようとしたら、そもそもLinkUPしてなかったので、とりあえずNIC自動起動に設定したメモ*1*2

#NICの開始
[root@localhost ~]# nmcli dev connect eno16777736
Device 'eno16777736' successfully activated with '314879d4-8571-4178-8874-6f1dea2bcc32'

#自動起動を有効に
[root@localhost ~]# nmcli con mod eno16777736 connection.autoconnect yes

#以下で自動起動にならないのはなぜなんだ…
#nmcli device set ifname eno16777736 autoconnect yes

#設定の確認
[mk55@localhost ~]$ nmcli con show eno16777736 | grep autoconnect
connection.autoconnect:                 yes
connection.autoconnect-priority:        0
connection.autoconnect-slaves:          -1 (default)

参考

2.3. NetworkManager のコマンドラインツール nmcli の使用

オフィシャルのドキュメントなのでそれなりに参考になる。

RHEL7のNICのネーミングルール - めもめも

全然知らなかったけど、RHEL7ではNICのネーミングルールが「Predictable Network Interface Names」変わっていて、イーサネット(オンボード)はenoで表示されるそうだ。

*1:なんだか同じ操作をするのに複数のやり方があって大変紛らわしい。ひとまずnmcli con downするとインターフェースが手動になるそうなので、nmcli dev~で操作した方が良いのだろうか

*2:コードをインライン表示すると脚注記法が効かなくなる

scriptコマンドで取得したログを綺麗に読む

scriptで取得したログにエスケープシーケンス含まれる場合、単純なテキストとして読むとエスケープシーケンスがESC~のような表示されてしまい読みづらい。 が、less -Rで読むと綺麗に読める。

scriptなどでログを取るときに変な文字(制御文字)のない状態で読めるようにする - Qiita 元ネタは上記の記事

具体例

lessでの表示
[mk55@localhost ~]$ powerofESC[ESC[ESC[ESC[ESC[ESC[ESC[ESC[K^G^G^G^G^G^G^G^G^G^G^GnmclESC[ESC[KllESC[ESC[Ki
ESC[32mvirbr0: connected to virbr0ESC[0m
        bridge, 52:54:00:A1:4E:6E, sw, mtu 1500
        inet4 192.168.122.1/24

ESC[31meno16777736: disconnectedESC[0m
        "Intel 82545EM Gigabit Ethernet Controller (Copper) (PRO/1000 MT Single Port Adapter)"
        1 connection available
        ethernet (e1000), 00:0C:29:53:02:5E, hw, mtu 1500

ESC[2mlo: unmanagedESC[0m
        loopback (unknown), 00:00:00:00:00:00, sw, mtu 65536

ESC[2mvirbr0-nic: unmanagedESC[0m
        tun, 52:54:00:A1:4E:6E, sw, mtu 1500
less -Rでの表示
Script started on Sun 21 May 2017 01:07:50 AM JST
[mk55@localhost ~]$ ^G^G^G^G^G^G^G^G^G^G^Gnmcli
virbr0: connected to virbr0
        bridge, 52:54:00:A1:4E:6E, sw, mtu 1500
        inet4 192.168.122.1/24

eno16777736: disconnected
        "Intel 82545EM Gigabit Ethernet Controller (Copper) (PRO/1000 MT Single Port Adapter)"
        1 connection available
        ethernet (e1000), 00:0C:29:53:02:5E, hw, mtu 1500

lo: unmanaged
        loopback (unknown), 00:00:00:00:00:00, sw, mtu 65536

virbr0-nic: unmanaged
        tun, 52:54:00:A1:4E:6E, sw, mtu 1500

なお、上記ではシンタックスハイライトが効いてしまってわからなくなっているが、less -Rだとカラーエスケープシーケンスも理解して、ちゃんと画面表示に色がつく(lessの場合は単純な白黒表示)。

参考:エスケープシーケンスについて

  1. もう一度基礎からC言語 第47回 特殊な画面制御~コンソール入出力関数とエスケープシーケンス エスケープシーケンスによる画面制御
  2. エスケープシーケンスを体感する - ザリガニが見ていた...。

CentOS7でデフォルトログインをCUIにする

CentOS7などのsystemを利用するOSでデフォルトログイン方法をCUIに変えるにはディスプレイマネージャを無効にすればよい。

環境

[root@localhost updates]# cat /etc/redhat-release 
CentOS Linux release 7.3.1611 (Core) 

状態の確認

サービスの有効化を確認(2通り)

[root@localhost updates]# systemctl list-unit-files | grep gdm.service
gdm.service                                   enabled 
[root@localhost updates]# systemctl is-enabled gdm.service
enabled

サービスの起動状態を含めて確認

[root@localhost updates]# systemctl status gdm.service
● gdm.service - GNOME Display Manager
   Loaded: loaded (/usr/lib/systemd/system/gdm.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2017-05-06 10:26:23 JST; 2h 32min ago
  Process: 1013 ExecStartPost=/bin/bash -c TERM=linux /usr/bin/clear > /dev/tty1 (code=exited, status=0/SUCCESS)
 Main PID: 934 (gdm)
   CGroup: /system.slice/gdm.service
           ├─ 934 /usr/sbin/gdm
           └─1042 /usr/bin/Xorg :0 -background none -noreset -audit 4 -verbose -auth /r...

May 06 10:26:23 localhost.localdomain systemd[1]: Starting GNOME Display Manager...
May 06 10:26:23 localhost.localdomain systemd[1]: Started GNOME Display Manager.
May 06 10:27:40 localhost.localdomain gdm[934]: Child process -2639 was already dead.

デフォルトのGDM(GNOME Display Manager)が有効になっている。

設定変更

サービスの無効化

[root@localhost updates]# systemctl disable gdm.service
Removed symlink /etc/systemd/system/display-manager.service.

サービスが無効なことを確認(2通り)

[root@localhost updates]# systemctl list-unit-files | grep gdm.service
gdm.service                                   disabled
[root@localhost updates]# systemctl is-enabled gdm.service
disabled

サービスの起動状態も含めて確認

[root@localhost updates]# systemctl status gdm.service
● gdm.service - GNOME Display Manager
   Loaded: loaded (/usr/lib/systemd/system/gdm.service; disabled; vendor preset: enabled)
   Active: inactive (dead)
 Main PID: 934
   CGroup: /system.slice/gdm.service
           ├─ 934 /usr/sbin/gdm
           └─1042 /usr/bin/Xorg :0 -background none -noreset -audit 4 -verbose -auth /r...

May 06 10:26:23 localhost.localdomain systemd[1]: Starting GNOME Display Manager...
May 06 10:26:23 localhost.localdomain systemd[1]: Started GNOME Display Manager.
May 06 10:27:40 localhost.localdomain gdm[934]: Child process -2639 was already dead.

GDMが無効化された。 なおこの時点では起動しているGDMGUIが停止するわけではない(GUI表示のまま)。

再起動(reboot)するとCUIログインに切り替わる

その他

なお、rebootコマンドはLinux以外のUnixだと危険なコマンドらしい。

Linuxについては、Man page of SHUTDOWNにあるように、rebootはshutdown -r -q nowのエイリアスです(シェルのエイリアスというわけではなく、内部でshutdown -r -q nowを呼び出しているだけと言うこと)。shutdownと同じくinitシステムを6に切り替えるため、サービスを順番に落とし(最新のLinuxではsystemdやupstartが採用される場合が多いですが、同じように順番にサービスを落とします)、再起動を行います。サービスが依存関係に基づいた安全な順序でおとされ、各終了処理も実行されますので、shotdownを使う場合と同じく、安全に再起動が可能です。

しかし、LinuxではないUNIXでは違います。次の二つを見てください。

FreeBSD 11.0 | On-line Manual of “reboot

Solaris 11.2 | reboot - マニュアルページセク ション 1M: システム管理コマンド  

FreeBSDSolarisでは、shutdownへのエイリアスででも呼び出す物でもありません。これらのrebootでは、initシステムの切り替えが発生せず、全てのプロセスに対して終了シグナルを送るだけです。サービスの終了順序は守られませんし、initスクリプトに記載された終了時の処理も行われません。そのため、サービスによってはテンポラリファイルやPIDファイルが残ったり、エラーが記録されたり、最後の情報が書き込まれなかったり等の不具合が起きる可能性があります

出典:Linux - shutdownコマンドとrebootコマンドの違いについて(56240)|teratail