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

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 743 articles
Browse latest View live