Quantcast
Channel: 俺的備忘録 〜なんかいろいろ〜
Viewing all 1028 articles
Browse latest View live

awkでgetlineを使ってファイルからn行づつデータを取得させる(別のファイルからデータを取得して差し込みetc…)

$
0
0

先日行われたシェル芸勉強会の中で、別のファイルから1行ずつデータを取得させたいということがあった。
いろいろとやり方を調べたりしていたのだが、その中でくんすとさんという方がいい感じの回答をしていた。

cat speech | awk 'NF==0{getline< "speech2"}{print}'#シェル芸

実行例を書いてみる。
以下の例では、「seq.txt」の空行に「aiueo.txt」というファイルの中身を差し込んでいっている。

[blacknon@thinkpad25][~/Work/201910/20191031]
(`・ω・´)  < cat seq.txt
1

2

3

4

5

6

7

8

9

10

[blacknon@thinkpad25][~/Work/201910/20191031]
(`・ω・´)  < cat aiueo.txt
あ
い
う
え
お
か
き
く
け
こ
                                                                                                                                                                             
[blacknon@thinkpad25][~/Work/201910/20191031]
(`・ω・´)  < cat seq.txt | awk 'NF==0{getline<"aiueo.txt"}1'
1
あ
2
い
3
う
4
え
5
お
6
か
7
き
8
く
9
け
10
こ

 

awkのgetline自体は、過去にOSのコマンド実行時にその結果をawkで取得するために何度か使っていたのだけど、ファイルを1行ずつ読み込むのにも使えたのはちゃんと理解できてなかった。
(というか、こういう使い方が本来あるべき姿なのかもだけど)

 

【参考】

 

「シェル芸」に効く!AWK処方箋 「シェル芸」に効く!AWK処方箋

awkで巨大な数字を取り扱う

$
0
0

先日のシェル芸勉強会において、awkで巨大な数字を取り扱うという内容があった。
awkでは、巨大な数字を扱おうとすると以下のようなエラーが出力されることがあるのだけど、これを回避する方法について考えるという内容だった。

$ echo|awk '{print(999^300)}'
+inf

 

Stack Overflowとかを漁っていると、どうも53bit分までのデータしか扱えないため、それ以上の数字になるとinfとして扱われてしまっている模様。

Very strangely, I found that in awk, the big integer looks like has only 53 bits.Here is my example:function bits2str(bits,data, mask){ if (bits == 0) return "0" mask = 1 fo...
Does big integer in AWK only have 53 bits? - Stack Overflow

 

1. gawkを使う場合

よく利用されているLinuxディストリビューションやMacで通常利用されているawkはGNU AWKになると思うのだが、awkにもいくつか種類がある。
で、そのうちのgawkなら-Mオプションを付与することで大きな数字を使った処理が可能になる。(よく使われているディストリビューションならgawkは最初から入っていると思う)

echo|gawk -M '{print(999^300)}'
$ echo|gawk -M '{print(999^300)}'
740707032156099464825492750125498782923874997114130170806961001465301947538727830598812870497467597333002429369381746413743021176620344898655207751051108708579287880019440359948243215080720463734669832067616376175341390825421213050005452748768407493902025373936876451797400970604198304852801661619040441348084547289450744175249347732702399917029521145703104814593938370882400665603350003537154659459564176427697064774096972412730972085462563537635168332560169315559224328701513052621237294170155723669297217400292632571256751580897716347362490700335631129107897897453613649989596616742433021575662612278745780586699903776370240325588753171590515699672270878538542792753250573616314228667632063973166085995878058059053688620177939547636928933182592888373343798594262891750605297854013386330575193419486800497723897287405936162673840121331123962104618131416258947795731421639516864193226719944849700001

2. 外のコマンドに処理させる

処理している内容にもよるが、もしbcなどの外部に出せる処理ならコマンドを組み立てて処理するという方法もあるだろう。
普通にWEBシステムとかそういうのだとコマンドインジェクション作り込む発想だからあんまりよろしくない方法だと思うんだけど、awkを使う場面って大体コンソールでの操作だと思うのでまぁこの場合は影響ないだろう(多分)。

echo|gawk -M '{print("999^300")}'|bc
echo|gawk -M '{print("bc <<<999^300")}'|bash
$ echo|gawk -M '{print("999^300")}'|bc
74070703215609946482549275012549878292387499711413017080696100146530\
19475387278305988128704974675973330024293693817464137430211766203448\
98655207751051108708579287880019440359948243215080720463734669832067\
61637617534139082542121305000545274876840749390202537393687645179740\
09706041983048528016616190404413480845472894507441752493477327023999\
17029521145703104814593938370882400665603350003537154659459564176427\
69706477409697241273097208546256353763516833256016931555922432870151\
30526212372941701557236692972174002926325712567515808977163473624907\
00335631129107897897453613649989596616742433021575662612278745780586\
69990377637024032558875317159051569967227087853854279275325057361631\
42286676320639731660859958780580590536886201779395476369289331825928\
88373343798594262891750605297854013386330575193419486800497723897287\
40593616267384012133112396210461813141625894779573142163951686419322\
6719944849700001
                                                                                                                                                                             
$ echo|gawk -M '{print("bc <<<999^300")}'|bash
74070703215609946482549275012549878292387499711413017080696100146530\
19475387278305988128704974675973330024293693817464137430211766203448\
98655207751051108708579287880019440359948243215080720463734669832067\
61637617534139082542121305000545274876840749390202537393687645179740\
09706041983048528016616190404413480845472894507441752493477327023999\
17029521145703104814593938370882400665603350003537154659459564176427\
69706477409697241273097208546256353763516833256016931555922432870151\
30526212372941701557236692972174002926325712567515808977163473624907\
00335631129107897897453613649989596616742433021575662612278745780586\
69990377637024032558875317159051569967227087853854279275325057361631\
42286676320639731660859958780580590536886201779395476369289331825928\
88373343798594262891750605297854013386330575193419486800497723897287\
40593616267384012133112396210461813141625894779573142163951686419322\
6719944849700001

 

基本的にはgawkで処理するのがいいと思う。
諦めてPerlでやるという方法もあるけど、そこは状況に応じて臨機応変に対応する感じだろうか…。

 

「シェル芸」に効く!AWK処方箋 「シェル芸」に効く!AWK処方箋

xargsから呼び出したコマンドにキーボード入力を渡す

$
0
0

Twitterをボケーッと眺めていたところ、xargsで呼び出したコマンドにキー入力をしたいという内容があったので、どうやったらできるのか調べてみた。

通常、対象のプロセスで標準入力が開いてないとだいたいの(というか、普通の)コマンドは処理を待たずに終了してしまう。なので、なんとかしてターミナルを標準入力として渡してやればいいということになる。

1. /dev/ttyを読み込ませる

以下のように、/dev/ttyをxargsで呼び出したコマンド内部で読み込ませる事で、ターミナルの入力を渡すことができる。

command... | xargs -I@ sh -c '</dev/tty command... @'

 

例えば、以下のようにすることでxargsで受け付けたファイルに対して対話式にrmをすることができる。

ls -1 | xargs -I @ sh -c '</dev/tty rm -i @ '

2. xargsの-aオプションを使用する

この話題を会話していたツイートで、xargsの-aオプション(ファイルからxargsで処理するデータを受け付けるオプション)を利用する方法もあるよという内容をおしえてもらった。

 

つまり、パイプから受け付けると標準入力がそのまま使えないので、-aでファイルとしてプロセス置換を利用してコマンドの結果を取得して、それをxargsからコマンドに渡すことで標準入力をそのまま使えるよ、という仕組みになる。
以下のような感じでコマンドを実行することになる。

xargs -I@ -a <(command...) command_in_xargs... @

 

rmでやった場合はこんな感じになるようだ。
command…にxargsに渡す出力を渡すコマンドが入る。

xargs -I@ -a <(command...) rm -i @

 

rmとかの単純なものであれば、xargs側で-pオプション(–interactive)を使って実行可否を確認するとかすればいいのだろうけど、コマンド個別の内容に回答するのならばこの方法が便利な気がする。

 

【参考】

 

難読化シェル芸の世界 ~Bashとすてきな難読化~ (プレミアムブックス版) 難読化シェル芸の世界 ~Bashとすてきな難読化~ (プレミアムブックス版)

awk(gawk/mawk)でaccesslog内の日付(yyyy/mm/dd HH:MM:SS)にn時間追加したり、フォーマットを変換して出力させる

$
0
0

たまにlogを見ながら作業をしたりすることがあるのだけど、そういうときサーバによってはログの日付がGMTで記録されてたりする。
ここは日本であり、やはり自分もJSTで普段生活しているので、いちいち脳内で+9時間して読み取るのはちょっとだるい。できればJSTになってる時刻でログを読みたいところ・・・。

というわけで、取得したログをJSTに変換して出力する方法について考えてみることにした

1. awkで日付を変換する(外部コマンド使用)

apacheのログなんかだと、デフォルトでは日付の表記が「28/Nov/2019:09:03:19」のようになってしまっている。
これだとちょっとawkでは扱いづらい面があるため、dateコマンドに食わせる事で対処する(Macの場合はgdateにする必要があるので注意)。個人的な好みで日付のフォーマットも変えてるのだけど、dateコマンドに食わせる場合だとそのあたりで自由が効く。

cat sample.log | awk '{"date -d \"$(echo \""$4"\"|sed -e\"s,/,-,g\" -e\"s,\\[,,\" -e\"s,:, ,\") 9 Hours\" \"+%Y-%m-%d %H:%M:%S\""|getline x;$4="["x;$5="+0900]";print $0}'
[blacknon@BlacknonMacBook-Pro2018][~/Work/201912/20191201]
(`・ω・´)  < tail sample.log
180.159.128.48 - - [01/Dec/2019:12:53:13 +0000] "GET /item/networking/4202 HTTP/1.1" 200 69 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0.1) Gecko/20100101 Firefox/9.0.1"
120.78.161.104 - - [01/Dec/2019:12:53:13 +0000] "GET /category/toys HTTP/1.1" 200 61 "/category/computers" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
140.153.157.210 - - [01/Dec/2019:12:53:13 +0000] "GET /category/electronics HTTP/1.1" 200 136 "-" "Mozilla/5.0 (Windows NT 6.0; rv:10.0.1) Gecko/20100101 Firefox/10.0.1"
120.45.206.66 - - [01/Dec/2019:12:53:13 +0000] "GET /item/electronics/1771 HTTP/1.1" 200 81 "/search/?c=Electronics" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"
112.225.194.42 - - [01/Dec/2019:12:53:13 +0000] "GET /category/networking HTTP/1.1" 200 105 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; YTB730; GTB7.2; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; Media Center PC 6.0)"
44.156.113.66 - - [01/Dec/2019:12:53:13 +0000] "GET /item/toys/2218 HTTP/1.1" 200 133 "/item/networking/3747" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
184.114.94.28 - - [01/Dec/2019:12:53:13 +0000] "GET /category/electronics HTTP/1.1" 200 128 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.77 Safari/535.7"
60.90.52.188 - - [01/Dec/2019:12:53:13 +0000] "GET /item/books/2219 HTTP/1.1" 200 81 "-" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
160.126.224.217 - - [01/Dec/2019:12:53:13 +0000] "GET /category/networking HTTP/1.1" 200 55 "/category/software" "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3"
116.126.93.220 - - [01/Dec/2019:12:53:13 +0000] "GET /category/games HTTP/1.1" 200 91 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; YTB720; GTB7.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"

[blacknon@BlacknonMacBook-Pro2018][~/Work/201912/20191201]
(`・ω・´)  < tail sample.log | awk '{"gdate -d \"$(echo \""$4"\"|sed -e\"s,/,-,g\" -e\"s,\\[,,\" -e\"s,:, ,\") 9 Hours\" \"+%Y-%m-%d %H:%M:%S\""|getline x;$4="["x;$5="+0900]";print $0}'
180.159.128.48 - - [2019-12-01 21:53:13 +0900] "GET /item/networking/4202 HTTP/1.1" 200 69 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0.1) Gecko/20100101 Firefox/9.0.1"
120.78.161.104 - - [2019-12-01 21:53:13 +0900] "GET /category/toys HTTP/1.1" 200 61 "/category/computers" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
140.153.157.210 - - [2019-12-01 21:53:13 +0900] "GET /category/electronics HTTP/1.1" 200 136 "-" "Mozilla/5.0 (Windows NT 6.0; rv:10.0.1) Gecko/20100101 Firefox/10.0.1"
120.45.206.66 - - [2019-12-01 21:53:13 +0900] "GET /item/electronics/1771 HTTP/1.1" 200 81 "/search/?c=Electronics" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"
112.225.194.42 - - [2019-12-01 21:53:13 +0900] "GET /category/networking HTTP/1.1" 200 105 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; YTB730; GTB7.2; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; Media Center PC 6.0)"
44.156.113.66 - - [2019-12-01 21:53:13 +0900] "GET /item/toys/2218 HTTP/1.1" 200 133 "/item/networking/3747" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
184.114.94.28 - - [2019-12-01 21:53:13 +0900] "GET /category/electronics HTTP/1.1" 200 128 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.77 Safari/535.7"
60.90.52.188 - - [2019-12-01 21:53:13 +0900] "GET /item/books/2219 HTTP/1.1" 200 81 "-" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
160.126.224.217 - - [2019-12-01 21:53:13 +0900] "GET /category/networking HTTP/1.1" 200 55 "/category/software" "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3"
116.126.93.220 - - [2019-12-01 21:53:13 +0900] "GET /category/games HTTP/1.1" 200 91 "-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; YTB720; GTB7.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"

 

その他、dateutilsのstrptimeコマンドやdateaddコマンドを利用する方法もある。
strptimeコマンドやdateaddkコマンドでは、以下のように時刻のフォーマットや追加する時刻を指定して加工ができる。

echo "12/Nov/2015:23:28:22"|strptime -i "%d/%b/%Y:%H:%M:%S" -f "%d.%m.%Y %H:%M:%S"
echo "12/Nov/2015:23:28:22"|dateadd -i "%d/%b/%Y:%H:%M:%S" -f "%d.%m.%Y %H:%M:%S" +9h

 

dateutilsを使った場合のサンプルは以下。
こちらのほうが指定がわかりやすい気がする。

tail sample.log | awk '{"echo \""$4"\"|dateadd -i \"[%d/%b/%Y:%H:%M:%S\" -f \"%Y/%m/%d %H:%M:%S\n\" +9h"|getline x;$4="["x;$5="+0900]";print $0}'

2. awkのmktimeやstrftimeを使って変換する

あまりデフォルトではないと思うが、YYYY/MM/DD HH:MM:SSといったフォーマットの場合であれば、awk(gawk/mawk)だけで対処できる(類似のフォーマットも可)。
gawk/mawkではmktime(UNIXTIMEを生成)やstrftime(UNIXTIMEを元に日付文字列を生成)という関数が用意されているので、それを利用する方法だ。

cat hogehoge.log | awk '{t=$1" "$2;gsub(/[:/]/," ",t);mt=mktime(t)+3600*9;jt=strftime("%Y/%m/%d %H:%M:%S",mt);$1=$2="";print jt" "$0}' | sed 's/ / /'
[DOCKER][root@bbc3073c335e][/]
(`・ω・´) < date -d '2019/12/01 00:00:00' '+%Y/%m/%d %H:%M:%S hogehoge' | awk '{t=$1" "$2;gsub(/[:/]/," ",t);mt=mktime(t)+3600*9;jt=strftime("%Y/%m/%d %H:%M:%S",mt);$1=$2="";print jt" "$0}' | sed 's/  / /'
2019/12/01 09:00:00  hogehoge

上のやり方では、UNIXTIMEに+3600*9で9時間足してやることでJSTに変換している。

awkの他にもperlやrubyを使って同様の処理ができそうだけど、とりあえずawkでの処理の方法について書いてみた。
以外と使うときがある…かもしれない(´・ω・`)。

 

「シェル芸」に効く!AWK処方箋 「シェル芸」に効く!AWK処方箋

コンソール上でGETパラメータ付きのURL一覧から各PATHごとに使われているパラメータを集計する

$
0
0

ごくごくたまにではあるのだが、GoogleやらBingの検索結果を一覧で取得することがあったのでそういうスクリプトをこさえて対応していた。
で、その実行結果で特定のドメインの検索結果からPATHごとにどのようなGETパラメータが使われているか集計したいということがあった。

具体的に言うと、例えば自分がよくお世話になっている上田先生のブログ(https://b.ueda.tech/)で検索かけて、その結果となるURLの一覧を抽出したとする(上田先生勝手にスミマセン…)。
その場合、以下のようなURLの一覧が取得されるのだが、そのPATHごとにどのようなGETパラメータをのパラメータ名を集計するのが今回やることになる。

[blacknon@thinkpad25][~/Work/201912/20191202] 
(`・ω・´)  < websearch search -P socks5://localhost:18080 -t google yahoo bing -T 'site:b.ueda.tech' | tee b.ueda.tech.list
Google Text Search: site:b.ueda.tech Yahoo Text Search: site:b.ueda.tech Bing Text Search: site:b.ueda.tech -> No more links Bing
-> Finally got 0 links Bing
-> No more links Google
-> Finally got 300 links Google
[GoogleSearch]: 上田ブログ: 近況: https://b.ueda.tech/
[GoogleSearch]: 12月に開催する2つの勉強会について個人でも宣伝し ... - 上田ブログ: https://b.ueda.tech/?post=01630
[GoogleSearch]: pmat version 0.001 | 上田ブログ: https://b.ueda.tech/?post=00697
[GoogleSearch]: 詳解確率ロボティクスのハンズオン勉強会をします | 上田ブログ: https://b.ueda.tech/?post=20191003_sice
[GoogleSearch]: jus共催 第39回シェル芸勉強会リンク集 | 上田ブログ: https://b.ueda.tech/?post=20181223_shellgei_39_links
[GoogleSearch]: 【問題のみ】jus共催 第30回危念シェル芸勉強会 | 上田ブログ: https://b.ueda.tech/?post=10188
[GoogleSearch]: 昔のJavaのソース漁り - 上田ブログ: https://b.ueda.tech/?post=01344
[GoogleSearch]: 千里浜にクシクラゲがたくさんいた | 上田ブログ: https://b.ueda.tech/?post=08570
[GoogleSearch]: 日記(シェル芸フル動員のデータ集計) | 上田ブログ: https://b.ueda.tech/?post=05251
[GoogleSearch]: Raspberry Piで始めるかんたんロボット製作 | 日経Linux | 上田 ...: https://b.ueda.tech/?page=05983
...

このURLの一覧から、各PATHごとに使われているGETパラメータについてを集計する場合、以下のようにコマンドを実行すれば良い。

cat b.ueda.tech.list | sed -r 's/.+: //;s/=[^=&]+/=XXX/g' | sort -u | awk -F'?' '{url=$1;c=split($2,p,"&");for(i=0;i<=c;i++){print url": "p[i]}}' | sort -u
[blacknon@thinkpad25][~/Work/201912/20191202] 
(`・ω・´)  < cat b.ueda.tech.list | sed -r 's/.+: //;s/=[^=&]+/=XXX/g' | sort -u | awk -F'?' '{url=$1;c=split($2,p,"&");for(i=0;i<=c;i++){print url": "p[i]}}' | sort -u
https://b.ueda.tech/: 
https://b.ueda.tech/: page=XXX
https://b.ueda.tech/: post=XXX

 

URLの一覧がどこかにあれば、同じような処理をすることでGETパラメータの集計ができるだろう。

 

1冊ですべて身につくHTML & CSSとWebデザイン入門講座 1冊ですべて身につくHTML & CSSとWebデザイン入門講座

bash/zshでhistoryファイル以外のファイルに実行コマンドやPWD、タイムスタンプを記録する

$
0
0

ふとした思いつきで、bashやzshのhistoryファイル(通常は環境変数のHISTFILEで指定したファイル)以外のファイルに、任意のフォーマットで実行したコマンドや実行時間、カレントディレクトリといった情報を記録できないだろうかと思ったので、試してみることにした。
今回はファイルに出力しているが、中の処理を書き換えれば実行履歴をDBに記録することもできると思うので、もし利用する場合は使い方に応じて適宜書き換えて貰えればいいだろう。

なお、この方法は単純にシェルの設定で対処しているだけの方法なので、ログの記録を強制できるというものではない(回避することは可能なはず)。
もし実行コマンドを強制的に記録させたいといった場合は、Snoopy Loggerのようなロギングツールを利用する方が良いだろう。

1. BASH_COMMANDを用いた処理方法(微妙…)

実行中のコマンドを取得するといえば、bashにはBASH_COMMANDという環境変数が用意されている。
trapで処理をしないとただ実行したコマンドが格納される(BASH_COMMANDをechoした場合、そのコマンドがそのまま格納される)というちょっと扱いにくい気のする環境変数なのだが、これを使った場合の方法を試してみる。

以下のような、trapでカレントディレクトリのファイルにBASH_COMMANDを出力するコマンドを実行して操作をしてみる。

trap 'echo "$BASH_COMMAND" >> ./test.log' DEBUG

 

で、結果としては以下のような事になってしまう。
実行コマンドが出力されるのでaliasが展開された状態となっており、さらにパイプライン単位で出力がされてしまい、パイプでつながってたのかどうかもここからだとわからない状態。

実行したコマンド: 
  $ ll
記録された内容: 
  ls --color=auto -la

実行したコマンド: 
  $ ll | grep abc
記録された内容: 
  ls --color=auto -la
  grep --color=auto abc

 

これだと、正直あまり嬉しいとは言えない。
できればパイプやセミコロン区切りをした場合でも1つのコマンド行としてログに出力してほしい。

2. accept-lineを置き換えて処理してみる(成功)

上記のように、BASH_COMMANDを使う方法だとパイプライン単位でしかコマンドを取得できないため、望んだ結果が得られない事はわかった。
じゃ他になにか方法が無いかと考えてみた結果、keybindのC-m(通常はaccept-line)の内容を置き換えて、accept-lineの実行前にREADLINE_LINE変数の内容(zshの場合はBUFFER)をログに出力させることで実行するコマンドを取得できることがわかった。

実際にサンプルを残しておく。

2-1. bashの場合

以下の内容をbashrcに記述することで、~/bash_history_$$.logに実行コマンドと時刻、カレントディレクトリが記録される。

function __accept-line() {
  if [ ! ${#READLINE_LINE} -eq 0 ]; then
    local log_file=~/bash_history_$$.log

    # mkdir
    mkdir -p ${log_dir}

    # logファイルへコマンドの出力
    date "+TimeStamp: %Y-%m-%d %H:%M:%S" >>${log_file}
    echo "CurrentDir: ${PWD}" >>${log_file}
    echo "Command: $READLINE_LINE" >>${log_file}
    echo "==========" >>${log_file}
  fi
}

# \C-mのキーバインドを変更する
bind -x '"\1299": __accept-line'
bind '"\1298": accept-line'
bind '"\C-m": "\1299\1298"'

 

2-2. zshの場合

zshの場合はこちら。
こちらも同様にzshrcに記載することで~/zsh_history_$$.logにその内容が記録される。

__accept-line() {
    # BUFFERのサイズに応じて処理を切り替える
    if [ ! ${#BUFFER} -eq 0 ];then
        local log_file="zsh_history$$.log"

        # mkdir
        mkdir -p ${log_dir}

        # logファイルへコマンドの出力
        date "+TimeStamp: %Y-%m-%d %H:%M:%S" >>${log_file}
        echo "CurrentDir: ${PWD}" >>${log_file}
        echo "Command: $READLINE_LINE" >>${log_file}
        echo "==========" >>${log_file}
    fi

    # accept-lineを実行
    zle .accept-line
}
zle -N accept-line __accept-line

 

もうちょっとカスタマイズすれば、正常終了/異常終了についても同様に記録することが可能になると思う。

 

フルスクラッチから1日でCMSを作る シェルスクリプト高速開発手法入門 改訂2版 フルスクラッチから1日でCMSを作る シェルスクリプト高速開発手法入門 改訂2版

GNU tarでアーカイブ内のファイルをgrep(相当の処理を)する

$
0
0

よく作業用のディレクトリとかを一定期間ごとにtar(+gzip)でアーカイブしてあるのだが、たまに過去のデータを調べるためにそのアーカイブしたファイルの中を検索したい事があったりする。
そういった際にいちいちアーカイブを展開して調べるのは面倒なので、なんかいい方法が無いかなと考えていたところ、どうやらGNU tarだとアーカイブしたファイル内のファイルに対してコマンドを実行させることができるらしい。
(残念ながら、BSD tarだとこの機能は無いみたいだ。残念。。。)

「–to-command」で指定したコマンドに対し、標準入力でtar内のファイルの中身を渡すことができるらしい。
なので、環境変数でtar内のどのファイルなのかも出力させることができる。

ただ標準入力から受け付けてるので、tar内のどのファイルに指定した文字列が含まれているか?といった場合だと–to-commandにgrepを指定する方式はちょっとめんどくさい。
こういった場合は、↓のようにawkで処理するのが楽みたいだ。

tar xf 対象となるアーカイブファイル --to-command "awk '//{printf(\"%s:%s\\n\",ENVIRON[\"TAR_FILENAME\"],\$0)}'"
$ # アーカイブ内のファイルの一覧を出力
$ tar tf test.tar.gz
a.txt
d.txt
g.txt
$
$ # 行頭にファイル名を付与して中身を出力
$ tar xf /home/blacknon/Work/201912/20191220/work/test.tar.gz --to-command 'sed "s,^,${TAR_FILENAME}: ,g";echo ===='
a.txt: a01 a02 a03 a04 a05
a.txt: b01 b02 b03 b04 b05
a.txt: c01 c02 c03 c04 c05
====
d.txt: d01 d02 d03 d04 d05
d.txt: e01 e02 e03 e04 e05
d.txt: f01 f02 f03 f04 f05
====
g.txt: g01 g02 g03 g04 g05
g.txt: h01 h02 h03 h04 h05
g.txt: i01 i02 i03 i04 i05
====
$
$ # a01で検索
$ tar xf test.tar.gz --to-command "awk '/a01/{printf(\"%s:%s\\n\",ENVIRON[\"TAR_FILENAME\"],\$0)}'"
a.txt:a01 a02 a03 a04 a05

 

あまり使う機会はそう多くなさそうだけど、使いこなせたら便利そうだ。
tarの–to-command内で利用できる環境変数はいろいろと種類があるようなので、いろいろと使ってみると良さそう。

 

【参考】

 

Commande Unix: Commandes Unix, Crontab, Make, Gnu Core Utilities, Tar, Fsck, Gnu Make, Chmod, Grep, Cat, Test, DD, Fortune, Hdparm, D Commande Unix: Commandes Unix, Crontab, Make, Gnu Core Utilities, Tar, Fsck, Gnu Make, Chmod, Grep, Cat, Test, DD, Fortune, Hdparm, D

第45回シェル芸勉強会に参加してきました(復習)

$
0
0

先日実施された第45回シェル芸勉強会に出席してきたので、その復習。
前回の45回はawkでゴリゴリ解いていくような問題が多かったのだけど、今回はいろんなコマンドを組み合わせて解いていくような問題が多めになっているらしい。

問題および模範解答はこちら。あと、問題を解くに当たって必要になるファイルは以下のコマンドで取得してくる。

git clone https://github.com/ryuichiueda/ShellGeiData

 

Q1.

csvファイル「data.csv」に、日別のトマト・バナナ・ピーマンの売れた個数が書かれているので、それぞれが記録されている最後の日の日付と個数を出力しろ、という問題。

[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < cat data.csv 
2019/12/6,�g�}�g,3��
2019/11/23,�o�i�i,2��
2019/11/8,�s�[�}��,31��
2019/12/30,�g�}�g,4��
2019/11/2,�g�}�g,1��
2019/12/9,�o�i�i,4��
2019/12/21,�o�i�i,5��
2019/11/21,�s�[�}��,32��
2019/12/1,�g�}�g,7��

Shift-JISになっているので、まずはiconvなりnkfでUTF-8に変換し、そこから日付ごとにsortしてトマト・バナナ・ピーマンそれぞれの最後の行を取得してやれば良さそうだ。
sortにはGNU sortに用意されているバージョンソート(-V)を利用するのが楽そうなので、それで回答してみる。

nkf -w data.csv | sort -V | awk -F, '{a[$2]=$0}END{for(k in a)print a[k]}' | sort -V
[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv 
2019/12/6,トマト,3個
2019/11/23,バナナ,2個
2019/11/8,ピーマン,31個
2019/12/30,トマト,4個
2019/11/2,トマト,1個
2019/12/9,バナナ,4個
2019/12/21,バナナ,5個
2019/11/21,ピーマン,32個
2019/12/1,トマト,7個
[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv | sort -V
2019/11/2,トマト,1個
2019/11/8,ピーマン,31個
2019/11/21,ピーマン,32個
2019/11/23,バナナ,2個
2019/12/1,トマト,7個
2019/12/6,トマト,3個
2019/12/9,バナナ,4個
2019/12/21,バナナ,5個
2019/12/30,トマト,4個

[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv | sort -V | awk -F, '{a[$2]=$0}END{for(k in a)print a[k]}'
2019/12/21,バナナ,5個
2019/12/30,トマト,4個
2019/11/21,ピーマン,32個

[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv | sort -V | awk -F, '{a[$2]=$0}END{for(k in a)print a[k]}' | sort -V
2019/11/21,ピーマン,32個
2019/12/21,バナナ,5個
2019/12/30,トマト,4個

 

その後、ebanさんがsort時にバージョンソート利用時に逆順にして条件に合致する先頭行だけを出力することで対処するという手法を編み出していた。
GNU sortではソート処理に使用する列と区切り文字を指定することができるので、それを利用した方法みたいだ。多分これが一番きれいな手法っぽい。

cd *a/*45;nkf -w data.csv | sort -rV | sort -t, -uk2,2
[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv | sort -rV | sort -t, -uk2,2
2019/12/30,トマト,4個
2019/12/21,バナナ,5個
2019/11/21,ピーマン,32個

 

Q2.

日経のデータダウンロードページから日経平均株価の日次csvファイルをダウンロードしてきて、そのCSVから毎月の終値の最高値と最低値を取得してくる。
ちなみに対象のcsvファイルは以下のコマンドで取得できる(wgetでいいんだけど、使ってるシェル芸botのDockerイメージに入ってないのでcurlで対処)。

curl -s 'https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv'
curl -s 'https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv' > nikkei_stock_average_daily_jp.csv

 

終値は2列目にあるので、これを月別に最高値、最低値を取得してけばいい。
あまりawkを使いたくないところだけど、こういうのはawk使っちゃったほうが早そうだ。以下、解答例。

curl -s 'https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv' | \
  nkf -w | \
  sed -e{1,\$}d -e's/"//g' | \
  awk -F, '
    {
        split($1,m,"/");
        mo=m[1]"-"m[2];
        if(b[mo]==""){b[mo]=a[mo]=$2};
        if($2>a[mo]){a[mo]=$2;}
        if($2<b[mo]){b[mo]=$2}
    }
    END{
        for(k in a)print k,a[k],b[k]
    }' | sort -V
[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < curl -s 'https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv' | \ >   nkf -w | \
>   sed -e{1,\$}d -e's/"//g' | \
>   awk -F, '
>     {
>         split($1,m,"/");
>         mo=m[1]"-"m[2];
>         if(b[mo]==""){b[mo]=a[mo]=$2};
>         if($2>a[mo]){a[mo]=$2;}
>         if($2<b[mo]){b[mo]=$2} >     }
>     END{
>         for(k in a)print k,a[k],b[k]
>     }' | sort -V
2016-01 18450.98 16017.26
2016-02 17865.23 14952.61
2016-03 17233.75 16085.51
2016-04 17572.49 15715.36
2016-05 17234.98 16106.72
2016-06 16955.73 14952.02
...

 

もうちょい良いやり方がありそうだけど、ひとまずこれで良しとする。

 

Q3.

ファイル「flags_a」「flags_b」には旗の絵文字が入っているが、何箇所かに違いがある。
この2つのファイルを比較して、左上から数えて何番目に違いがあるかを出力するという問題。

「左上から数えて」というところに引っかかるけど、要は何文字目の値が違うかを比較してやればいいので、まず1行で1文字ごとになるように分解してやり、diffをしてやればいい。
旗の絵文字はUnicodeの結合絵文字になるので、普通にgrepとかで1文字を指定してしまうとうまくいかないため、(汎用性はないけど)2文字指定して対処する。あとは、diffのオプションで出力方式を切り替えてやる。

eval diff '--old-line-format="%dn: %L" --new-line-format="%dn: %L" --unchanged-line-format=' '<(grep -o .. flags_'{a,b}')'
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < eval diff '--old-line-format="%dn: %L" --new-line-format="%dn: %L" --unchanged-line-format=' '<(grep -o .. flags_'{a,b}')'
39: 
65: 

 

Q4.

「ワタナベ」のナベで使われている漢字がひたすら記述されているファイル「nabe」に対し、一文字ごとに改行を入れるという問題。
ワタナベのナベは漢字にかなり種類があって、Unicodeは異体字クラスタを利用しているため単純に一文字指定だとうまく処理できないのを利用した問題のようだ。

とりあえず、当日では以下のような方法で対処してみた。

cat /ShellGeiData/vol.45/nabe | sed 's/[亜-熙][^亜-熙]*/&\n/g'

 

その後、鳥海さんが書記素クラスタを利用した簡単な方法を披露。
さすがにPerlは強い…。

perl -C -nle 'print $& while (/\X/g)' nabe
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < perl -C -nle 'print $& while (/\X/g)' nabe
部
邊
邊
邊
邉
邉
邉
邊
...

 

その後、ふとgrepの-Pオプションでも書記素クラスタ使えるのではないかと思って試してみたところ、うまく動いた。
試してみるものである(´・ω・`)。

grep -Po '\X' nabe

Q5.

後半戦。
テキストファイル「message」から、回文になっている箇所をすべて抜き出すという問題。
まずすべての文字の組み合わせを抽出する必要があるので、そこから対処する。
ひとまず、以下のようにseqやawkを利用して文字の組み合わせを取得する。

cat message|(a=$(cat);seq -f 'echo '${a}'|grep -o .|awk "NR>%g{a=\$0=a\$0;print}"' 0 ${#a})|bash|grep -E '..+'
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < cat message|(a=$(cat);seq -f 'echo '${a}'|grep -o .|awk "NR>%g{a=\$0=a\$0;print}"' 0 ${#a})|bash|grep -E '..+'
きつ
きつつ
きつつき
きつつきと
きつつきとま
きつつきとまと
きつつきとまとへ
...

 

あとは、これらの中から回文を抽出してやればいい。
このあたりはもうperlを使ってしまったほうが楽そうだ。

cat message|(a=$(cat);seq -f 'echo '${a}'|grep -o .|awk "NR>%g{a=\$0=a\$0;print}"' 0 ${#a})|bash|grep -E '..+'|perl -C -lne 'print $_ if ($_ eq reverse($_))'
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < cat message|(a=$(cat);seq -f 'echo '${a}'|grep -o .|awk "NR>%g{a=\$0=a\$0;print}"' 0 ${#a})|bash|grep -E '..+'|perl -C -lne 'print $_ if ($_ eq reverse($_))'
きつつき
つつ
とまと
とまと
けやぶやけ
やぶや
たけやぶやけた
けやぶやけ
やぶや
ゆんゆ
んゆん
おかしがすきすきすがしかお
かしがすきすきすがしか
しがすきすきすがし
がすきすきすが
すきす
すきすきす
きすき
すきす

 

なお、すべての組み合わせの抽出については鳥海さんが大変きれいな正規表現を残してくれてたので、それについても残しておく。

perl -C -lne '/..+(?{print$&})(?!)/' message

 

これを使って回文を抽出した場合が以下。
(多分もっと短く書けるんだろうけど)これだけでもだいぶ短くなった(やっぱperlはすごい)。

perl -C -lne '/..+(?{print$&})(?!)/' message|perl -C -lne 'print $_ if ($_ eq reverse($_))'

 

Q6.

Q5の回答が入っているファイル「message.ans」というファイルから、部分的な回文を除外するという問題。
部分的な回文と言われて何すれば良いのかよくわかってなかったのだけど、どうやらmessage.ans内から「きつつき」の一部である「つつ」に相当するような回文を消すという内容らしい。

つまりmessage.ans内の各文字列でgrepによる抽出を行い、一度しかヒットしないものだけを抽出すればいいということになる(複数回ヒットする回文は回文内の文字列である可能性が高いため)。
以下が回答。

cat message.ans | xargs -I@ grep -o @ message.ans  | sort | uniq -u
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < cat message.ans | xargs -I@ grep -o @ message.ans  | sort | uniq -u
おかしがすきすきすがしかお
きつつき
たけやぶやけた
とまと
ゆんゆ
んゆん

 

Q7.

x/yが割り切れない自然数の組x,y (x<y)について、echo x yからはじめて、計算結果(小数)を延々と出力しなさいという問題。
小数点以下を延々と計算し続けろという内容になる。楽しようとしてなんか良いコマンドないかと探してたのだけど見つからず、残念ながら解けなかった。

とりあえず模範解答を貼っておく。

echo 1 7 | awk '{print "0.";while(1){print int($1*10/$2);$1=($1*10)%$2}}' | tr -d \\n

 

Q8.

Q7の結果に対して、循環小数になっているのでその小数点以下の出力を途中で打ち切って小数何桁目から循環しているのかを出力するという問題。
こちらもちょっと解けなかったので、模範解答を貼る。

echo 1 7 | awk '{print "0.";while(1){printf int($1*10/$2);$1=($1*10)%$2;print " "$1}}' | awk '{b[NR-1]=$2;for(i=1;i<NR-1;i++){if(b[i]==b[NR-1]){print a, i,NR-2;exit}}a=a$1}'

 

いつものことではあるが、後半の問題が難易度が高いのであまり解けない。
今回は比較的前半の問題が易しめ(というよりいろんな解き方がある)だったように感じたので、いろいろな解き方を見れて勉強になった。

 

フルスクラッチから1日でCMSを作る シェルスクリプト高速開発手法入門 改訂2版 フルスクラッチから1日でCMSを作る シェルスクリプト高速開発手法入門 改訂2版

Viewing all 1028 articles
Browse latest View live