Quantcast
Channel: Bashタグが付けられた新着記事 - Qiita
Viewing all 2850 articles
Browse latest View live

うっかり創業者のコードを上書きしない技術

$
0
0

この記事は SmartHR Advent Calendar 2019 14日目の記事です。

はじめに

私は 3人目あるいは 4人目のエンジニアとして、 2016年2月に SmartHR に入社しました。
あれから約4年、今となっては順調に権限移譲の進んでいる SmartHR ですが、当時は正社員が 7人しかおらず、全社員が様々な仕事を行う小さな企業でした。
当然エンジニアである副社長の @kakipoはプロダクトコードを書いていますし、非エンジニアである @miyasho88も SmartHR にコードを書いては PR を投げていました。

社長による PR
image.png

こういった、創業者がコードに込めた思いというのはとても重要であり、おいそれと上書きしてよいものではありません。
この記事では、入社から 4年弱の期間、私がいかにして創業者のコードを上書きしないで済んだのか、その方法をお伝えします。

うっかり上書きしないために

さて、では実際にどのようにしてコードの上書きを避けるのか説明します。その答えはいたって簡単で、 Git Hook にスクリプトを仕込むことで解決します。
commit する際に修正箇所の最終編集者を確認、創業者として指定したアカウント名が変更に含まれている場合はエラーを出して commit を中止する、という流れになります。
このスクリプトを実行するためには、創業者が commit に利用したアカウント名がわかる必要がありますので、それはなんとかして手に入れてください。

なお、本記事で扱うコードはほぼデフォルトの macOS Catalina 上でのみ動作を確認しておりますので、そのほかの環境では正常に動作しない可能性がありますのでご了承ください。

処理の流れ

スクリプト内で行われる処理の流れとしては以下のようになります。

  1. git hook の pre-commit でスクリプトが実行されます
  2. git diff から変更のあった行を取得します
  3. git blame を使って変更のあった行を誰が書いているのか確認します
  4. 指定したユーザ名が含まれる行を変更している場合にはメッセージを出力して git commit を中止します

コード

プロダクト直下から .git/hook/pre-commitに以下のコードを追記します

BOSS_NAMES=YOUR_BOSS_NAME # カンマ区切りで複数指定可能ですfile_names=(`git diff --cached--name-only`)for file in${file_names[@]};do
    git_diffs=(`git --no-pager diff --cached--no-ext-diff-U1000000${file} | $(cd$(dirname$0)&&pwd)/diff-lines.sh | grep-E"^[^\"].*\:[0-9]+\:\-"`)for git_diff in${git_diffs[@]};do
        line=(`echo${git_diff//:/}`)origin=`git blame -L${line[1]},+1 HEAD^ ${file} | awk'{print $2}' | sed-e's/^(\(.*\)/\1/'`if[[${BOSS_NAMES}==*${origin}*]];then
            echo"You will overwrite ${origin}'s code in *****:${line[1]}"is_boss_exist=true
        fi
    done
done

if["${is_boss_exist}"];then
    echo"Commit aborted."exit 1
fi

なお、ここでは簡単のために、 gitで変更ファイルの差分行番号を取得するには? - どこでも見れるメモ帳で書かれているコードをお借りして行番号を取得しています

そのため、上記のブログで使われている diff-lines.sh.git/hookディレクトリに保存し、 chmod +x .git/hookとして実行権限を付与する必要があります

実際の動作

output.gif

ちゃんと abort されましたね :tada:
どこで誰のコードを消したり上書きしようとしているか一目瞭然です。便利。

最後に

今回はうっかり創業者のコードを上書きしないために Git Hook を使ってコミットを中断させました。
しかし、動画作成のためにいろいろと探してみたのですが、 社長のコードはほぼ残っておらず、結果的として 2015年に作られた 12番目の PullRequest をチェックアウトすることとなりました。 とても残念です
いつの間にか大量に書き換えていた事実に驚愕です。

記憶があれば、来年は おまえは今まで書き換えた創業者のコードをおぼえているのか?をお送りいたします。

明日じゃなくて来年です。みなさま良いお年を!

参考


tukubaiコマンドを使っていい感じに集計して表示する

$
0
0

はじめに

みなさまは「ユニケージ開発手法」をご存知ですか?

知らないですよね。

ユニケージ開発手法では100種類ほどのtukubaiコマンドと言われる独自コマンドを駆使してテキストデータの加工を行い、データベースやWebのデータ処理を行うことができます。

何をするのか

今回はそんなユニケージ開発手法でよく使われるtukubaiコマンドを使用して簡単なデータの集計とhtmlの作成をしていきたいと思います。

ユニケージ開発手法についての詳細を知りたい方は以下記事を御覧ください。
ハンズラボが採用しているユニケージという謎テクノロジーについて 第1回

本記事でのコマンドの実行環境は以下のとおりです。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.2
BuildVersion:   18C54
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
Copyright (C) 2007 Free Software Foundation, Inc.
$ python -V
Python 3.6.0

コマンドを導入する

主要なコマンドに絞ったオープンソース版があるので実際に試したい方は以下から導入してみてください。
Open usp Tukubaiダウンロード

集計したいデータを用意する

ユニケージでは、テキストファイルを用います。

今回は店舗のマスタと各店舗の一日の売上をまとめたファイルを用意します。

店マスタ

1フィールド目:店ID
2フィールド目:店名

shop
000001 A店
000002 B店
000003 C店
000004 D店
000005 E店

各店の一日の売上データ

1フィールド目:店ID
2フィールド目:1会計の売上(円)

tran
000001 2688
000005 3117
000005 6848
000004 8121
000005 4017
000005 5992
000003 4094
000004 519
・・・以下同じようなのが続きます

集計する

まずは売上データの小計を求めていきます。

$ sm2

ファイルから、キーが同じ行の各フィールドの値を集計できます。
・ キーの範囲(下記の例だと1フィールド目)と集計したいフィールドを指定(下記の例だと2フィールド目)

$ cat tran  | LANG=C sort-k1,1 | sm2 1 1 2 2
000001 43819
000002 22602
000003 16437
000004 32260
000005 59126

店マスタと連結する

$ join1

keyで指定したフィールドがmasterの1フィールド目と一致した行を連結して出力します。

$ cat tran | LANG=C sort-k1,1 | sm2 1 1 2 2 | join1 key=1 master -
000001 A店 43819
000002 B店 22602
000003 C店 16437
000004 D店 32260
000005 E店 59126

売上ランキングを出す

$ juni

順位を出力できます。
ソート機能はないので直前にソートしておきます。

$ cat tran | LANG=C sort-k1,1 | sm2 1 1 2 2 | join1 key=1 master - | sort-k3,3r | juni
1 000005 E店 59126
2 000001 A店 43819
3 000004 D店 32260
4 000002 B店 22602
5 000003 C店 16437

カンマを付ける

売上などの数値は大きくなりがちなので3桁ごとにカンマを入れていきます。
そんなときにもtukubaiコマンドが便利です。

$ comma

指定したフィールドに3桁ごとのカンマを入れることができます。
カンマを入れたあとは数値計算ができなくなるので最後に使用するのがポイントです。

$ cat tran | LANG=C sort-k1,1 | sm2 1 1 2 2 | join1 key=1 master - | sort-k3,3r | juni | comma 4
1 000005 E店 59,126
2 000001 A店 43,819
3 000004 D店 32,260
4 000002 B店 22,602
5 000003 C店 16,437

HTMLを作成する

集計はできましたがテキストデータのままだと見づらいのでExcelかHTMLで表示させたいところです。
今回はHTMLのテーブルに集計したデータを表示させていきます。

$ mojihame

テンプレートとなるファイルにmojihameこみます。

まずはテンプレートとなるhtmlを作成します。

template.html
<tableborder="1"><trbgcolor="#9ad1ff"><th>順位</th><th>店コード</th><th>店名</th><th>売上</th></tr><!-- LABEL --><tr><tdalign="center">%1</th><td>%2</th><td>%3</th><td>%4</th></tr><!-- LABEL --></table>

そしてtemplate.htmlに対してmojihameを行います。

template.htmlの%1,%2,%3...に当たる部分が売上データの各フィールドに置換されます。
このとき-lオプションで文字列を指定した場合、template.htmlの文字列で囲まれた行(今回はLABELに囲まれた行)にデータをはめ込みます。

$ cat tran | LANG=C sort -k1,1 | sm2 1 1 2 2 | join1 key=1 master - | sort -k3,3r | juni | comma 4 | mojihame -lLABEL template.html
<table border="1">
  <tr bgcolor="#9ad1ff">
    <th>順位</th>
    <th>店コード</th>
    <th>店名</th>
    <th>売上</th>
  </tr>
  <tr>
    <td align="center">1</th>
    <td>000005</th>
    <td>E店</th>
    <td>59,126</th>
  </tr>
  <tr>
    <td align="center">2</th>
    <td>000001</th>
    <td>A店</th>
    <td>43,819</th>
  </tr>
  <tr>
    <td align="center">3</th>
    <td>000004</th>
    <td>D店</th>
    <td>32,260</th>
  </tr>
  <tr>
    <td align="center">4</th>
    <td>000002</th>
    <td>B店</th>
    <td>22,602</th>
  </tr>
  <tr>
    <td align="center">5</th>
    <td>000003</th>
    <td>C店</th>
    <td>16,437</th>
  </tr>
</table>

ブラウザで表示させてみます。
スクリーンショット 2019-12-14 14.13.10.png
いい感じに表示できましたね。

まとめ

いくつかのコマンドを使っただけで集計からHTMLの作成までできました。

今回紹介したコマンド以外にもopen版では50種類、ライセンス版では100種類ほどの便利コマンドがあります。

興味を持たれた方は是非調べて見てください。

明日は@watarukuraさんです!

今年のコマンド使用頻度ランキングを算出して1年を振り返ってみよう

$
0
0

今年も終わりが近づいてきたので、自分がよく使うコマンドをランキング形式にして振り返ることができたら面白そうと思ったのでやってみました。

結論

各種コマンドの使い方がBashかZshか、GNU系のOS(Linux)かBSD系のOS(Macなど)かで微妙に変わってきます。
実行コマンド例は以下のとおりです。

累計でベスト10を出してみる

# Bash x BSDの場合 (※ $HISTTIMEFORMAT='%F %T')history\
  | awk'{ for (i = 4; i < NF; i++) { printf("%s ", $i) } print $NF }'\
  | sed's/|/\'$'\n/g'\
  | awk'{ print $1 }'\
  | sort\
  | uniq-c\
  | sort-r\
  | head
# Zsh x GNUの場合history 1 \
  | awk'{ for (i = 2; i < NF; i++) { printf("%s ", $i) } print $NF }'\
  | sed's/|/\n/g'\
  | awk'{ print $1 }'\
  | sort\
  | uniq-c\
  | sort-r\
  | head

Bashの場合historyに引数を指定する必要はなく、2行目のawkのiは4から始まります。(環境変数HISTTIMEFORMATが%F %Tのとき)
Zshの場合historyの引数に1が指定され、2行目のawkのiは2から始まります。
また、awkがBSDのものかGNUのものかによって4行目のawkの指定の仕方が異なります。

2019年に限定してベスト10を出してみる

単にgrepを使うだけです。細かい期間指定をしたい場合はegrepを使いましょう。

# Bash x BSDの場合 (※ $HISTTIMEFORMAT='%F %T')history\
  | grep 2019- \
  | awk'{ for (i = 4; i < NF; i++) { printf("%s ", $i) } print $NF }'\
  | sed's/|/\'$'\n/g'\
  | awk'{ print $1 }'\
  | sort\
  | uniq-c\
  | sort-r\
  | head

Zshで期間指定する場合はhisotry-iオプションを追加します。
3行目のawkのiの開始は4に変わっています。

# Zsh x GNUの場合 (※ $HISTTIMEFORMAT='%F %T')history-i 1 \
  | grep 2019- \
  | awk'{ for (i = 4; i < NF; i++) { printf("%s ", $i) } print $NF }'\
  | sed's/|/\n/g'\
  | awk'{ print $1 }'\
  | sort\
  | uniq-c\
  | sort-r\
  | head

ちなみに私の自宅Macでの実行結果はこんな感じでした。
職場のMacでやってみるとまた結果が変わって面白いと思います。

 201 vi
 148 ll
 111 git
  96 cd
  87 gc
  79 kg
  73 rm
  72 k
  66 brew
  61 mv

ちなみに、aliasされたコマンドはvi='nvim'll='ls -al'gc='git commit'kg='kubectl get'k='kubectl'です。

解説

上記のコマンドはほとんど同じですが微妙に違うところがあります。
BashかZshかでhistoryの使い方や結果が異なりますし、BSDかGNUかでsedの使い方が異なります。

history

まずはBashとZshで微妙に仕様が違うhistoryについて見ていきましょう。
Bashでそのままコマンドを打ってみるとこのように表示されます。

$ history
    1  2018-09-08 17:00  brew install ricty
    2  2018-09-08 17:05  cp -f /usr/local/opt/ricty/share/fonts/Ricty*.ttf ~/Library/Fonts/
    3  2018-09-08 18:29  brew search thunderbird
    ...

日付時刻付きで全件表示されます。
一方Zshで同様に打ってみると・・・

 2593  ./init.sh
 2594  ll ~/Library/Application\ Support/Code/User/
 2595  gc -m "Add VSCode keybindings"
...
 2610  git init

日付や時刻は表示されず最新16件のみ表示されます。
ただし、Bashのhistoryでも日付や時刻が表示されない場合があります。
それは環境変数HISTTIMEFORMATが設定されていない場合です。
この場合は(途中で設定したとしても)期間ごとに絞り込むことはできないので注意してください。
以降はHISTTIMEFORMATに%F %Tが設定されているという前提で話を進めます。

Bashはデフォルトで全件出ますし、HISTTIMEFORMATを指定していれば日付も出ますのでこれ以上詳しく話す必要もないでしょう。
Zshのhistoryの場合、全件出すには引数を指定する必要があります。

# 全件表示(1件目以降全て)history 1
# 区間指定history 10 20

また、先程説明したとおり日付時刻を表示に含めるには-iオプションを付け加えます。
Zshのhistoryのオプションについてより詳しく知りたくばman zshbuiltinshistoryの項目とfc -lの項目を見てください。
そう、Zshのhistoryfc -lのaliasなのだそうです。

       history
              Same as fc -l.

historyの結果をコマンド種別で集計する

一旦間を飛ばして、シンプルにhistoryの結果をコマンドごとに集計するにはどうすればいいか考えてみます。
Bashのhistoryで説明します。
historyの1行ごとの出力は空白文字区切りで

インデックス 日付 時刻 コマンド サブコマンドやオプションなど

となっているので、パイプなどを考慮しなければawk '{ print $4 }'に渡せばよいことがわかります。
そうしたら、uniq -cで重複部分を消しつつカウントすればいいようですが、uniqは連続した重複しか消すことができないので、uniq -cする前に一旦sortします。

# Bash$ history | awk'{ print $4 }' | sort | uniq-c
...
   1 c
   3 cal
  31 cat
 116 cd
  13 chmod
   5 code
   1 command
   7 cp
  60 curl
  10 cut
 ...

あとはこれを降順にソートし直せば頻度順になります。

パイプした後のコマンドも集計に含めたい

パイプした後のコマンドも集計に含めたい場合どうすればいいでしょうか。
方法は色々あると思いますが、今回は|を改行コードに置換するという方法を取りました。
ここでsedがGNUかBSDのものかで置換の指定方法が若干異なります。

# BSD sedsed's/|/\'$'\n/g'# GNU sedsed's/|/\n/g'

BSDのsedで改行コードに変換する方法を調べていたら以下の記事が見つかりましたので、詳しく知りたければ読んでみてください

ただし、普通に改行するだけではコマンド部分とインデックスやサブコマンド以降と切り分けづらいです。
なので改行する前にコマンド部分以前をカット(コマンド部分以降を抽出)してしまいたいです。
それが

awk'{ for (i = 4; i < NF; i++) { printf("%s ", $i) } print $NF }'

の部分に当たります。
解説しておきながら実はawkの言語仕様については詳しくないので、forの中身がfor (i = n; i <= NF; i++)でなかったり、通常最後列の文字列が入る$NFにfor pirntfした内容が入っている理由を知っている方がいたら教えていただけると助かります。

つまり、ここまでの流れをまとめると

# Bash x BSDの場合 (※ $HISTTIMEFORMAT='%F %T')history\
  | awk'{ for (i = 4; i < NF; i++) { printf("%s ", $i) } print $NF }'\
  | sed's/|/\'$'\n/g'\
  | awk'{ print $1 }'\
  | sort\
  | uniq-c\
  | sort-r\
  | head

のコマンドはおおよそ

コマンド履歴を表示する \
  | 余分な列をカットする \
  | パイプ後もカウントするよう改行コードに置換する \
  | 重複カウントしやすいようコマンド部分のみを抽出する \
  | 重複カウントしやすいようソートして重複数をカウントする \
  | 頻度順にソートする

という意味になります。

おまけ

historyの最大件数は環境変数HISTSIZEで決まっています。
コマンド履歴は今回のような使い方以外にもCtrl + Rでの検索時に重宝しますので、CUI操作をよくする人はより大きい数を設定しておくとより便利になります。

そして普段Macを使っている人でGNU sedを使いたい人はbrew install gnu-sedしてalias sed='gsed'を.bashrcか.zshrcに書き加えておきましょう。
その他GNUコマンドのインストール方法に関しては、こちらの記事がとてもよくまとまっていました。

macOS CatalinaにUpdateしたら, gitの補完が効かなくなったので直した

$
0
0

macOSのアップデートをしてmacOS Catalinaに変更した時に,terminalでのgithubの補完機能が働かなくなってしまったので,修正をしました.というより,一から導入したので,その話をします.

OSのアップデートをすると,ディレクトリの構成などが変わる場合があります.私はgitのterminalでの補完機能を次のディレクトリから呼び出していました.

  • /Library/Developer/CommandLineTools/usr/share/git-core/git-completion.bash
  • /Library/Developer/CommandLineTools/usr/share/git-core/git-prompt.sh

OSのアップデートをしたら,上記のファイルがなくなったみたいなので,本記事では,1から補完機能を導入することにしました.

1から導入する

  • 使用した環境
$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.15.2
BuildVersion:   19C57

ファイルの準備

  • git-prompt.shのダウンロード
wget https://raw.githubusercontent.com/git/git/master/contrib/completion/git-prompt.sh -O ~/.git-prompt.sh
  • git-completion.bashのダウンロード
wget https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.bash -O ~/.git-completion.bash

それぞれのファイルは,~/に保存されています.vim ~/.git-prompt.shvim .git-completion.bashで内容を確認することができます.

.bashrcに設定

次に,bashrcの設定を行います.

  • vim ~/.bashrcに次を追加してください
source ~/.git-prompt.sh
source ~/.git-completion.bash

追加したら,.bashrcを保存してください.

  • terminalで次のコマンドを入力してください.
$ source ~/.bashrc

最後に

補完機能があれば,typoが減って効率化できます.

IMPORTANT: You may need to close and restart your shell after running 'conda init'.

$
0
0

IMPORTANT: You may need to close and restart your shell after running 'conda init'.

Anacondaでの環境設定などを行なっている時に遭遇したエラーメッセージ

意味

'conda init'を実行した後に,terminalを閉じるかshellを再起動する必要があります.

症状

  • 通例通りに,anacondaで仮想環境を作成する
$ conda create -n <env_name>
  • 環境が作成できているか次のコマンドで確認する
$ conda info -e

正しく環境が作成できている

ここでエラー発生

  • 次のように,仮想環境をactivateしようとすると,エラーを出力した
$ conda activate <env_name>

エラーメッセージ

IMPORTANT: You may need to close and restart your shell after running 'conda init'.

エラーメッセージにしたがってコマンドを入力

  • エラーメッセージによると,conda init <shell_name>しろ!みたいに言ってきたので,まずは指示通りにする.
  • bashを使用していたので,次のコマンドを入力
$ conda init bash

のようにそのまま入力したところ,bash_profileを更新したとよ(modified)というメッセージが出てくる.

bachrcを再度読みこむ

$ source ~/.bachrc

ここは,新しいターミナルを開いても同じ結果になります.

これで完了

ランダムでUNIXコマンドを教えてくれる"cmdsay"を作ってHomebrewに公開するまで

$
0
0

はじめに

UNIXコマンドって多すぎて覚えられないですよね!そこで、ランダムでコマンドを教えてくれるコマンドcmdsayを作りました!
このコマンドは/usr/bin配下のコマンドとその説明をランダムで表示します!
.bashrc.zshrcにこのコマンドを書いてシェルログインのたびに実行するようにしておけば、あなたの知らないコマンドに巡り会えるかも!

Kyou13/cmdsay - GitHub
cmdsay.gif

この記事ではコマンドを作成し、Homebrewパッケージとして公開するまでの手順を記載しています。

(このコマンド、当初はシェル設定ファイルに直接シェルスクリプト書いており、パッケージとして公開するつもりはなかったのですが、アドベントカレンダーネタとしてつまらないかもと思い、Homebrewパッケージとして公開に至りました。)

コマンドの作成

シェルスクリプトでコマンドの作成を行います。

コマンドの流れは非常にシンプルです。
1. /usr/bin配下のコマンドをランダムで1つ選択
2. whatisでコマンドの説明を取得
3. 2.をcowsayに渡し、シェルに出力

この自作コマンドの中でメインとなっているコマンドはwhatiscowsayです。
whatisはコマンド検索を行うコマンドで、whatis コマンド名で該当コマンドと簡易的な説明が表示されます。
cowsaycowsay 文字列で、与えた文字列をキャラクターに喋らせます。

sortは引数として-Rを与えるとランダムソートを行います。

cmdsay.sh
#!/bin/shwhile : ;do# ランダムでコマンドを取得COMMAND="$(basename$(find /usr/bin -type f | sort-R | head-n 1))"# whatisでコマンドの説明を取得DESCRIPTIONS="$(whatis $COMMAND)"# 取得したコマンドに説明があるときif(echo$DESCRIPTIONS | grep-v"nothing appropriate"> /dev/null);then
    DESCRIPTION=$(echo$DESCRIPTIONS | grep"^$COMMAND" | sort-R | head-n 1)if[-n"$DESCRIPTION"];then
      break
    fi
  fi
done# cowsayのキャラクターをランダムで選択するCOW=$(basename$(ls-1 /usr/local/Cellar/cowsay/*/share/cows/*.cow | sort-R | head-n 1 | sed's/\.cow$//'))# 見やすいようにコマンドと説明の間に改行を入れて、最終的な出力をするecho$DESCRIPTION | sed's/ - /\'$'\n/' | cowsay -f$COW-nunset COMMAND
unset DESCRIPTIONS
unset DESCRIPTION
unset COW

工夫した点

すべてのコマンドにwhatisによる説明があるとは限りません。そのため、ランダムにコマンドを選択する処理を無限ループで包み、if (echo $DESCRIPTIONS | grep -v "nothing appropriate" > /dev/null);で説明があるコマンドのみを選択するようにしています。

また、whatisコマンド名 - 説明というフォーマットで結果を返すのですが、引数として1つのコマンドを与えても、複数の結果が返ってくることがあります。これは与えたコマンドに関係するコマンドも返すためです。
例えば、whatis pythonを実行するとpythonの他に関係するコマンドのpydocpythonwも返ってきます。

$ whatis python
pydoc(1)                 - the Python documentation tool
python(1)                - an interpreted, interactive, object-oriented programming language
pythonw(1)               - run python script allowing GUI

今回は1つのコマンドの説明を返したいため、この中からから更にランダムで1つ選択するような処理もしています。

cowsay-fオプションでキャラクターを指定できます。今回はキャラクターもランダムで表示するようにしています。

自作コマンドをHomebrewパッケージの公開

macのパッケージマネージャHomebrewでは、brew tapを使うことで、公式以外のformulaを追加でき、これによって自作のコマンドやアプリを簡単に公開することが可能です。
formulaとは、ビルド方法を記載したrubyファイルのことです。

パッケージ公開の手順としてはざっくり以下のように行います。
1. 公開するコマンドを作成
2. formulaファイルを作成
3. githubにHomebrew配布用のリポジトリを作成
4. 自作コマンドとformulaファイルをアップロード

配布用リポジトリの作成

Homebrew配布用のリポジトリをgithubに作成します。
このとき注意する点として、リポジトリ名をhomwbrew-から始める必要があります。

また、本来はhomebrew-から始まるリポジトリを一つ用意すればよいのですが、今回は2つのリポジトリhomebrew-tapcmdsayを作成しています。homebrew-tapでformulaを管理し、cmdsayで自作コマンドのソースコードを管理するためです。

自作コマンドのソースコードをアップロードしたリポジトリで、リリースノートを作り、インストールリンクを作成した後、formulaファイルにインストールリンクを記載します。

自作コマンドをアップロード

自作したコマンドをgithubにアップロードします。今回はcmdsay.shをリポジトリcmdsayにpushします。また、後々自作したコマンドをアップデートをすることを想定し、タグを付けます。

cmdsay
$ git add cmdsay.sh
$ git commit -m'initial commit'$ git tag v0.0.1
$ git push origin v0.0.1

Releaseノートの作成

タグをpushしたら、リリースノートを作成します。リポジトリを開いてreleaseタグをクリックし、Releaseページに移動したら、Draft a new releaseをクリックします。
Releases_·_Kyou13_cmdsay.png

追加したタグを選択し、タイトルを入力します。そしたらページ下部のPublish releaseをクリックし、Releaseノートが出来ます。ソースコードのリンク先Source code (tar.gz)は次のformulaの作成で使用するためコピーしておきます。
Releases_·_Kyou13_cmdsay_.png

formulaの作成

brewコマンドにはformulaの雛形を生成するコマンドが存在します。引数にはダウンロードリンクを与えます。先程コピーしたものを与えてください。

$ brew create [ソースコードのリンク先]

するとエディタでformulaが開かれ、編集画面に移ります。
最低限必要なのはurl,sha256installメソッドです。
urlsha2561は設定済みなので、installメソッドを編集していきます。

installメソッドでビルド手順を記載します。
bin.installにはビルドの結果得られた実行ファイルを指定します。そうすることで、/usr/local/bin配下にシンボリックリンクを作成できます。

また、他のformulaと依存関係がある場合はdepends_onを指定する必要があります。
今回はcowsayとシェルスクリプトをコンパイルするshc(shell script compiler)を指定します。

最終的なformulaは以下のようになります。

classCmdsay<Formuladesc"random show cmd description"homepage""url"https://github.com/Kyou13/cmdsay/archive/v0.0.1.tar.gz"sha256"[生成されたsha256ハッシュ]"depends_on"cowsay"depends_on"shc"definstallsystem"shc","-f","cmdsay.sh","-o","cmdsay"bin.install"cmdsay"endend

このformulaをhomebrew-tap/cmdsay.rbとして保存したら、githubのformula管理用リポジトリにアップロードします。

自作コマンドをbrewでインストールし確認する

実際にアップロードした自作コマンドをインストールします。

$ brew tap [githubユーザ名]/tap
$ brew install[githubユーザ名]/tap/[自作コマンド名]

私の場合は以下のようになります。

$ brew tap Kyou13/tap
$ brew install Kyou13/tap/cmdsay

コマンドが実行できるかを確認したら、完了です!

おわりに

自作パッケージの公開は敷居が高そうと思っていました。しかし、今回始めてHomebrewパッケージを公開してみて、とても簡単な手順で公開することが出来ました!


  1. sha256が設定済みではない場合やファイルの更新を行った場合はopenssl dgst-sha256 [filename]を実行して取得する 

AWSのIPレンジを調べる

「社員名簿」のダミーデータをシェル(とFaker)で作る

$
0
0

シェル芸アドベントカレンダー2019に空きがあったので、最近やったシェルの話で参加させていただきます。

自分は日経Linux誌でシス管系女子というシェルの解説記事(マンガ)を連載させていただいてます。その2020年1月号掲載分の回において、「劇中の架空の会社で、アダムズ方式を使って各部署から代表者を何名かずつランダム且つ公平な感じで選ぶシェルスクリプトを作る」という話をやっています。

アダムズ方式では、前の計算の結果を使って次の計算を行うというステップが何度か出てきます。今までは「1画面分だけの画面出力」が必要なケースが多かったので都度てきとうにその場の気分で偽の画面をデッチ上げていたものの、こう複雑な話になってくると、それぞれの画面間で数字に矛盾が無いようデッチ上げるのはなかなか大変です。なので、実際にそれっぽい「社員名簿」のダミーデータのCSVを作って、その処理結果をそのまま使うことにしました。

こういう場面でのダミーデータの作り方はいろいろあると思いますが、描いているのがシェルの解説なので、シェルでやってみました。というのがこの記事の内容です。

作りたい物

用意するダミーデータの形式は「社員番号,氏名,勤務地(拠点),部署」の列を持つCSVという事にします。具体的には以下のような感じです。

members.csv
2,中島 悠太,仙台,法務部
4,後藤 凛,大阪,製造部
6,清水 優斗,東京,営業部
...
1600,利奈 みんと,東京,システム部
1601,野口 七海,仙台,開発部
1602,藤本 愛美,仙台,経理部
1603,青木 真央,大阪,CS部
1604,野村 奈々,東京,営業部
1605,河野 結,大阪,CS部

各フィールドは完全ランダムではなく、以下のような条件を満たす感じにしたいという思惑があります。

  1. 社員番号は連番にしたい。
    • 退職や転職などがあったという想定で、社員番号は適度に不連続にしたい。
    • 「特定の時期に退職者が集中している」みたいな事までは考えないことにする。
    • 最後の方の数十件くらいは、入って日が浅い人達という事で退職者無しで社員番号が連続するようにしたい(主人公のみんとちゃんもそのあたりに含めたいので)。
  2. 部署・拠点には偏りを持たせたい。
    • 単純に部署をランダムに割り振ると「えっシステム部が数百人規模!?」「本社と支社が同規模ってあり得るの?」みたいな事になってしまって不自然なので。
    • 「新部署創設で特定の時期に特定の部署の人が集中している」みたいな事までは考えないことにする。
  3. 特定の登場人物として、何人かは決まったID・名前・勤務地・部署にしたい。

全体の行数は劇中舞台の架空の会社の全従業員数ということになります。劇中では、全国に何カ所か拠点があるかなり大きなBtoCの会社っぽい描き方をしているので、そんな感じの会社を探して組織概要を見てみたら1000人前後とありました。これを参考にしつつ、キリのいい数字よりは半端な数字の方がそれっぽいかと思って1127人と決めました1

ステップ1:社員番号と名前だけの一覧を作る

  • 退職や転職などがあったという想定で、社員番号は適度に不連続にしたい。
  • 最後の方の数十件くらいは、入って日が浅い人達という事で退職者無しで社員番号が連続するようにしたい(主人公のみんとちゃんもそのあたりに含めたいので)。

この条件に合うデータにしたいので、多めに作って必要な人数分をランダムに抽出することにしました。あんまり退職者が多いとブラック企業っぽくなってしまうかなと思ったのですが、程度が分からなかったので、「退職者」まで含めた人数は無根拠でてきとうに1605件と決めました2

日本人ぽい人名をランダムに自動生成するツールというと、自分はRubyのgemでFaker使い方)という物があると以前に聞いた事があります。なので、人名部分だけはそれを使って簡単なスクリプトで生成してみました。

people.rb
#coding utf-8require'faker'# 日本語のデータセットを使うFaker::Config.locale=:ja# 引数で指定された回数実行するARGV[0].to_i.timesdo|count|puts"#{count+1},#{Faker::Name.name}"end

これを使って、

$ sudo gem install faker
$ all=1605
$ active=1027
$ newbie=20
$ ruby people.rb $all> members-all.csv # 退職者まで含めた一覧を用意$ cat members-all.csv | head-n -$newbie |
    shuf-n$(($active-$newbie)) |
    sort-n-k 1 -t , > members-base-oldies.csv # ベテラン勢$ cat members-all.csv | tail-n$newbie> members-base-newbies.csv # 新人勢$ cat members-base-oldies.csv members-base-newbies.csv > members-base.csv # 結合して「在籍者のリスト」にする
  • head -n -$newbieは、入力全体の中で「先頭から N - $newbie行」つまり「末尾の $newbie行を除いた残りの行」を出力する。
  • shuf -n $(($active - $newbie))は、入力をシャッフルした中から $active - $newbie行だけ取り出す。
  • sort -n -k 1 -t ,は、入力のカンマ区切りの1列目を数値と見なしてソートする。
  • tail -n $newbieは、入力全体の中で末尾から $newbie行を出力する。

という具合に、スクリプトの実行結果から「ベテラン勢」(最後の20件以外を取り出して、そこから1107件をランダムに抽出し、最初のカラムでソートした結果)と「新人勢」(最後の20件)を取り出して連結すれば、

2,中島 悠太
4,後藤 凛
6,清水 優斗
...

こんな要領の1127人の「現役社員」の一覧ができます。

ステップ2:勤務地と部署を付け足す

東京・大阪・福岡・仙台に拠点があって東京本社が一番人数が多い(人数比はとりあえず5:2:1:1と想定)ということにしたいので、数に偏りを持たせた拠点一覧を用意します。

regions.csv
東京
東京
東京
東京
東京
大阪
大阪
福岡
仙台

件数が少ないので、自分はvimでの手書きコピペで作りました。9行しかありませんが、実際にはこれを繰り返し使うので、1127人分に拡大してもそれぞれ同じ割合になります。

同様にして、人数に偏りを持たせた部署一覧を用意します。話の都合上多めに部署を用意したいので、聞いた事のある部署名を12個ほど列挙してみました。各部署の人数は、実際の大会社が実際どんな感じなのか自分は知らないので、想像でてきとうに決めました。

部署割合
法務部2%
システム部2%
デザイン部3%
人事部6%
総務部6%
資材部7%
経理部7%
広報部10%
CS部11%
開発部13%
製造部16%
営業部17%

件数が多くなるので、さっきの拠点一覧と同じ要領で手書きで用意するのはちょっと大変そうです。任意の回数同じ文字列を繰り返し出力するのは yes '文字列' | head -n 件数でできる3ので、whileを使って半自動生成します。

$ (cat<<__EOF__法務部 2
システム部 2
デザイン部 3
人事部 6
総務部 6
資材部 7
経理部 7
広報部 10
CS部 11
開発部 13
製造部 16
営業部 17
__EOF__
) | while read name count;do yes"$name" | head-n$count;done> divisions.csv

(ヒアドキュメントの記法的には

$ cat<<__EOF__ | while ...
...
__EOF__

とするのが「正しい」のですが、データが後から出てくる形式は自分は読みにくく感じるため、先にヒアドキュメントを書く書き方を好んでいます。)

こうして生成した元データを使って shuf -r -n $activeとすると、指定件数より少ない元データからでも、必要な件数分の出力を得られます。

$ cat regions.csv | shuf-r-n$active> regions-shuffled.csv
$ cat divisions.csv | shuf-r-n$active> divisions-shuffled.csv

それを先の社員番号と名前だけのCSVに列として付け足せば、名簿っぽいダミーデータのできあがりです。これは、与えた複数ファイルそれぞれの全行を列として横方向に結合する pasteコマンドで行います。

$ paste-d , members-base.csv regions-shuffled.csv divisions-shuffled.csv > members-with-details.csv
members-with-details.csv
2,中島 悠太,仙台,法務部
4,後藤 凛,大阪,製造部
6,清水 優斗,東京,営業部
7,小島 陽太,東京,経理部
10,伊藤 陸,東京,営業部
...

なお、shuf-r--repeat)オプションと -n 件数--head-count=件数)オプションを併用すると、「与えたデータの各行と同じ登場割合で指定件数の出力をランダムに得る」のではなく、「与えた元データをシャッフルして出力する、という処理を、結果が指定件数に達するまで繰り返す」という動作になります。そのため、結果の先頭の方にばかり「東京」がやたら出てくるみたいな偏り4は発生せず、拠点と部署は全体で満遍なく登場し、充分に件数が多ければ、各拠点におおむね全部署の人がいるという結果になります5。これは、出力結果を拠点ごとに絞り込んで部署名部分だけ取り出して uniqした結果の件数(=その拠点に一人でも人員がいる部署の数)を見ると確かめられます。

$ cat members-with-details.csv | grep 東京 | cut-d , -f 4 | sort | uniq | wc-l
12
$ cat members-with-details.csv | grep 大阪 | cut-d , -f 4 | sort | uniq | wc-l
12
$ cat members-with-details.csv | grep 福岡 | cut-d , -f 4 | sort | uniq | wc-l
12
$ cat members-with-details.csv | grep 仙台 | cut-d , -f 4 | sort | uniq | wc-l
12

「開発の人員を集めた拠点」とか「製造の部門を集めた拠点」とかを想定したい場合、データの作り方をまた工夫する必要があります。架空の会社の内情を想像し始めるときりが無いので、今回はそこまでは行わないことにしました。

ステップ3:特定の登場人物に差し替える

以上でダミーデータのCSVができたわけですが、劇中には既に何人か所属が明らかになっている人達がいます。推定される入社時期から社員番号をてきとうに決めて、こんな風にCSVを用意してみました。

members-specials.csv
512,谷町 列樹,東京,システム部
1024,大野 桜子,東京,システム部
1280,麻土 科学人,東京,開発部
1598,報伝 広宣,東京,広報部  ←広報に配属された同期
1599,営 優人,東京,営業部  ←みんとちゃんと間違えられた優秀な成績の同期
1600,利奈 みんと,東京,システム部

まず、先のダミーのCSVから社員番号がこれらと一致する行を取り除きます。grep-fオプションでパターンファイルを指定でき、条件を反転する -vオプションと組み合わせると「与えた除外リストに該当しない物だけ出力する」フィルターとして機能します。特別な人物のCSVの社員番号の列だけを取り出して除外リストにすれば、「社員番号がこれらの人物に一致しない人物だけのCSV」を得られます。

$ cat members-specials.csv | cut-d , -f 1 > special-ids.csv
$ cat members-with-details.csv | grep-v-f special-ids.csv > members-isolated.csv

そうしたら、それと特別な人物のCSVを連結して、再び社員番号順でソートし直します。

$ cat members-isolated.csv members-specials.csv | sort-n-k 1 -t , > members.csv

これで、「劇中描写にそれなりに則ったダミーの社員名簿」のできあがりです。

members.csv
2,中島 悠太,仙台,法務部
4,後藤 凛,大阪,製造部
6,清水 優斗,東京,営業部
...
1598,報伝 広宣,東京,広報部
1599,営 優人,東京,営業部
1600,利奈 みんと,東京,システム部
1601,野口 七海,仙台,開発部
1602,藤本 愛美,仙台,経理部
1603,青木 真央,大阪,CS部
1604,野村 奈々,東京,営業部
1605,河野 結,大阪,CS部

シェル芸ちゃうやん問題

以上がダミーデータの作り方の逐次解説となりますが、外部のスクリプトファイルを用意していたり、各ステップで一時ファイルをたくさん作っていたりとスマートでなく、シェル芸アドカレに参戦してるくせにシェル芸ちゃうやんけというツッコミを受けそうな気がしてきたので、最後にbashのプロセス置換6を使いまくってここまでの話をワンライナーにまとめた物を示してお茶を濁しておく事にします。

$ all=1605;active=1027;newbie=20;\specials="$(cat<<__EOF__
512,谷町 列樹,東京,システム部
1024,大野 桜子,東京,システム部
1280,麻土 科学人,東京,開発部
1598,報伝 広宣,東京,広報部 
1599,営 優人,東京,営業部 
1600,利奈 みんと,東京,システム部
__EOF__
)";\people="$(ruby -r faker -e"Faker::Config.locale = :ja; $all.times {|count| puts \"#{count+1},#{Faker::Name.name}\" }")";\cat\<(paste-d , \<(cat\<(echo"$people" | head-n -$newbie |
             shuf-n$(($active-$newbie)) |
             sort-n-k 1 -t ,)\<(echo"$people" | tail-n$newbie))\<((cat<<__EOF__東京 5
大阪 2
福岡 1
仙台 1
__EOF__
) | while read name count;do yes"$name" | head-n$count;done | shuf-r-n$active)\<((cat<<__EOF__法務部 2
システム部 2
デザイン部 3
人事部 6
総務部 6
資材部 7
経理部 7
広報部 10
CS部 11
開発部 13
製造部 16
営業部 17
__EOF__
) | while read name count;do yes"$name" | head-n$count;done | shuf-r-n$active) |
      grep-E-v"^($(echo"$specials" | cut-d , -f 1 | paste-d'|'-s))")\<(echo"$specials") | sort-n-k 1 -t ,

まとめ

シェルのコマンドとFakerの組み合わせでダミーデータのCSVを作る例をご紹介しました。

Fakerは完全にランダムなダミーデータを作るのに便利なのですが、多少偏りのあるダミーデータを用意しようと思うと工夫が必要です。Pure Rubyでももちろんこの記事に書いたような事はできますが、自由度が非常に高くて色々なやり方が考えられるので、「どこまでこだわるか」の見切りをつけるのが難しくて自分は却って混乱する感じがありました(実際、収拾が付かなくなって途中で放り投げてしまいました)。その点、シェルだとメジャーなコマンド群で簡単にできる事には限りがあり「簡単にできるレベルにとどめよう」という圧が自然とかかるので、入れ込みすぎないで済むというメリットがあるんじゃないかなという気がします。

この記事では各段階を迷い無く進めていますが、実際にやった時には、社員番号をランダムに抽出する部分までRubyでやってみたり、先に1605件の部署名込みのCSVを用意してから必要な件数を抽出してみたりと、あれこれ試行錯誤してどうにか希望するアウトプットを得られたという状況でした。この記事の内容は、それを後から振り返って「あっ、ここもっとこういう風に効率よくできたやん」と気付いた所をちまちま最適化した結果の、言うなれば「よそ行きのコマンド列」となっています。1つの事をやるにも解法が色々あり、期待する結果を得られていさえすればそのいずれも正解なのがシェルなので、シェルにまだ自信が無い方々は達人達の極まったシェル芸を見て萎縮してしまわず、自分なりのアプローチでスクラッチでコマンド列を組み立てる練習を重ねてみて下さい。

 

ところで、この記事で作った社員名簿には拠点の情報がありますが、日経Linux 2020年1月号掲載分の回の劇中データには拠点の情報がありません。実は、拠点の情報は日経xTECHで展開中の「シス管系女子」過去回よりぬき+新作Web連載の最後の1話で使う予定で改めて付け加えた物なのでした(どんな使い方をするかは本編を見てのお楽しみ)。ただ、スケジュール的に制作が〆切デッドラインに間に合うかどうかギリギリなので、もし最後の1話が過去回の再掲になっていたら「ああ、間に合わなかったんだな……」と笑ってやって頂ければ幸いです。


  1. この規模ならActive Directoryとかで認証基盤を組んでユーザーの管理もそこでやるのが当たり前なのでは?というツッコミもあろうと思いますが、なにぶん連載のテーマがシェルなので、「実はそういう技術がまだ発展していないパラレル時空だった」みたいな想像で補って頂けましたら幸いです。それか、たまたまCSVでエクスポートした物があったのでそれを使ったという話でもいいです。 

  2. 普通どれくらいの頻度で人が入って辞めていくのか分からなかったのですが、一般的基準に照らし合わせて離職者多すぎなのであれば、分社化したとかそういう風に想像で補って貰えれば幸いです…… 

  3. 他には echo '文字列' | shuf -r -n 件数というやり方もあります。 

  4. ランダムに決めるというのは「次の結果がどうなるか予想できない度合いが高い」という事なので、「必ず満遍なく登場する」という予想しやすい結果になる保証は無く、結果の部分部分を見ると偏った感じになる事があります。 

  5. 全体の件数が少ないと、特定の部署の人がいない拠点が出てきやすくなります。 

  6. <(コマンド列)と書くと、そのコマンド列の実行結果の出力を保存したファイルを指定したのと同じように扱われるという、bashの便利な機能。 


いつのまにかzshでもないbashでもないshになってた話

$
0
0

こないだ何も知らずにchshした後の続きで
しらーんまにKPバッシュっぽくなってて、コナンくんよろしく「あれれーおかしーぞー?」って思って、
スクリーンショット 2019-12-15 16.17.37.png
↑試行錯誤してたあとによーくターミナルみたら....
おまえ"sh(シェル)"やんwww

結局前と全く同じコマンドを打ってからウィンドウ開き直したら直りました

terminal
chsh -s /bin/zsh

あれ?.bash_profileや.bashrcが効いてないな?って思った時に読む記事

$
0
0

Macのバージョンアップに伴い、bashからzsh切り替わるとターミナルの$が、%になるんやけどここで気づいて欲しいのは僕らの「~/.bash_profile」はどうなったんやということ
bashからzshに移行メモにあるようにシンプルにとりあえず丸コピーでいいわっていう人はこれ

bash
$ cat .bash_profile >> .zprofile

なんやったら。あの上書きされると困る人もいるので一応追記コマンドにしてます

bash
$ cat .bashrc >> .zshrc

余談

あと余談なんやけどpyenvを入れた後に上記作業もやってると色々面倒で以下のようなことが起きてた
python2>3自動切り替えされんなんや分からんとりま「pyenv init」や

zsh
 ~ % pyenv init
# Load pyenv automatically by appending# the following to ~/.zshrc:eval"$(pyenv init -)"

親切に「~/.zshrcに追加しろ!」って教えてくれるんですね。
なので、cat使って一行で追加すると

echo "eval \"\$(pyenv init -)\""  >> .zshrc

前提環境:zsh

蛇足

あれお前profileに追記してないやんけという人はこちら
本当に正しい .bashrc と .bash_profile の使ひ分け

いまさらですがzshって2013年には切り替えよっていわれてたんですね
ヾ(o゚ω゚o)ノ゙bashからzshに移行して2014年を迎えよう! ref:
https://gist.github.com/harapeko/8045912

コマンド - とりあえずZshを使えば良いんだろう? - Qiita

ojichatグラフシェル芸

$
0
0

ojichatグラフシェル芸とは?

o.png
こういう画像を作るシェル芸です。

コマンド

以下のコマンドで目的の画像が出力できます。

$ echo"digraph{👴[fontsize=100]👴🏻[fontsize=100]👴🏼[fontsize=100]👴->👴🏻[label=\"`ojichat|grep -Eo'.{1,14}'`\"]👴🏻->👴🏼[label=\"`ojichat|grep -Eo'.{1,14}'`\"]👴🏼->👴[label=\"`ojichat|grep -Eo'.{1,14}'`\"]}"|dot -Tpng>/images/o.png

ojichatの出力がランダムであるため、エッジのキャプションは実行するたびに変わります。

前提条件

graphvizojichatがインストールされていること。

コマンド解説

3つのノードをエッジでつないだグラフを描きます。

$ echo"digraph{A->B B->C C->A}"|dot -Tpng>/images/0.png

0.png
エッジにキャプションを添えます。

$ echo"digraph{A->B[label=\"from A to B\"] B->C[label=\"from B to C\"] C->A[label=\"from C to A\"]}"|dot -Tpng>/images/1.png

1.png

ノードに👴(OLDER MAN)を入れます。

$ echo"digraph{👴->👴🏻[label=\"from 👴 to 👴🏻\"] 👴🏻->👴 🏼[label=\"from 👴🏻 to 👴🏼\"] 👴🏼->👴[label=\"from 👴🏼 to 👴\"]}"|dot -Tpng>/images/2.png

2.png
👴🏻と👴🏼は👴に skin tone modifierを付けたものです。

エッジのキャプションをojichatの出力にします。

$ echo"digraph{👴->👴🏻[label=\"`ojichat`\"]👴🏻->👴🏼[label=\"`ojichat`\"]👴🏼->👴[label=\"`ojichat`\"]}"|dot -Tpng>/images/3.png

3.png

ojichatの出力が長すぎるので改行します。grep -Eo '.{1,14}'として、14文字で改行します。

$ echo"digraph{👴->👴🏻[label=\"`ojichat|grep -Eo'.{1,14}'`\"]👴🏻->👴🏼[label=\"`ojichat|grep -Eo'.{1,14}'`\"]👴🏼->👴[label=\"`ojichat|grep -Eo'.{1,14}'`\"]}"|dot -Tpng>/images/4.png

4.png

👴のサイズを調整します。
graphvizfontsizeを指定します。

$ echo"digraph{👴[fontsize=100]👴🏻[fontsize=100]👴🏼[fontsize=100]👴->👴🏻[label=\"`ojichat|grep -Eo'.{1,14}'`\"]👴🏻->👴🏼[label=\"`ojichat|grep -Eo'.{1,14}'`\"]👴🏼->👴[label=\"`ojichat|grep -Eo'.{1,14}'`\"]}"|dot -Tpng>/images/o.png

o.png
完成です。

所感

これをシェル芸botに投げたとき、「地獄」という反応が多かったんですけど、僕としては「優しい世界」のつもりでやっていました。
以上です。

bashとcurlで実現する初めてのdotfiles

$
0
0

自分のdotfilesをちゃんと整理したいと思っていて、この週末にやっと作業したので、共有したいと思います。

下調べ

世の中には、どんなdotfiles管理手法があるのかあるのか調べました。
pythonスクリプトやrubyなど、言語も様々で、
リンクするか、コピーするか等、ファイルの配置方法も色々あることがわかりました。

私としては、最小構成のサーバーに対しても、すぐにdotfilesを展開できるような仕組みを作りたかったので、以下の方針で自分のdotfilesを整備することを考えました。

方針

  • できるだけシンプルな構成にする
  • bashだけでできる

参考にした記事、レポジトリ

以下の記事が共感でき、一番参考になりました。

作業

  • b4b4r07さんとyutalatayさんのinstallスクリプトを参考にして、以下の機能をもつスクリプトを作成しました
--- a/yutakatay/dotfiles-mini/.bin/dotsinstall.sh
+++ b/hnishi/dotfiles/scripts/install.sh
@@ -19,7 +19,7 @@ link_to_homedir() {
   fi

   local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-  local dotdir=$(readlink -f ${script_dir}/..)
+  local dotdir=$(dirname ${script_dir})
   if [[ "$HOME" != "$dotdir" ]];then
     for f in $dotdir/.??*; do
       [[ `basename $f` == ".git" ]] && continue
  • ワンラインのコマンド一発で、ダウンロードとインストールを行えるようにしました
curl -L raw.githubusercontent.com/hnishi/dotfiles/master/scripts/download.sh | bash
  • scriptsというディレクトリ名は、私の好みでつけました
  • できたdotfilesはこちら。
  • bashとcurlがあればワンコマンドでinstallできるdotfilesが完成しました

今後の展望

nohupとリダイレクトを自分のために調べてみた

$
0
0

この記事は本当に自分用のメモなので得るものはあまりないと思います。私が最初にリンクを貼っているブログを読まれると最高なので、みなさんはそちらを参照してください。

師匠のかずきさんのブログエントリに触発されて、私も簡単なことを馬鹿にせず、自分で調べて試してから物事を進めようとおもっています。時間が何倍もかかるのですが、これは最初は仕方ないと思うことにしました。ただ、コントロール出来ている感があるので、やっていくとスピードアップできそうです。

調査の背景

VMをプロビジョンするときに、スクリプトで様々な初期処理をしますが、定番のバックグラウンドジョブをプロビジョニングスクリプトから実行する場合、次のような構成にします。これは前からふわっと知っていますが、正直ちゃんと理解していないので、今回調べてみました。

$ nohup /foo/var.sh > some.log 1<2&

nohup コマンド

nohup コマンドは、SIGHUPがプロセスに送信されても無視するコマンドです。こちらのブログに詳細の解説があるのですが、例えば、あなたがターミナルからコマンドを実行していて、バックグラウンドプロセスとして実行するために、&をつけていたとしても、使っていたターミナルを落とすと大体その実行されていたプロセスは死んでしまいます。死ぬかどうかは、シェルや、ターミナルの実装によるのですが、死ぬ可能性が高いです。実際にBashなどは終了時に、バックグラウンドプロセス含めSIGHUPを発行してしまいますので、せっかくバックグラウンドで実行していても、死んでしまうことが発生します。ですので、バックグラウンドにするだけではなく、nohupをつけて、SIGHUPを無視すると落ちることは無いということです。

実行例

hello.sh

#!/bin/bashwhile true;do 
    echo"hello still alive"sleep 5
done

nohup なし

$ ./hello.sh > hello.log &
$ cat ./hello.log
hello still alive
hello still alive

ターミナルを殺す(私はVSCodeのインテグレーションターミナルでWSLのターミナルを殺しました。)

$ ps -ef | grep hello

死んでいます。

nohup あり

$ nohup ./hello.sh > hello.log &

ターミナル殺す

$ ps -ef | grep hello
s -ef | grep hello
ushio     8111     1  0 12:28 ?        00:00:00 /bin/bash ./hello.sh
ushio     8588  2058  0 12:30 pts/1    00:00:00 grep--color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svnhello

生きてます。

プロセスが停止中

ブログによるとバックグラウンドプロセスが停止中の場合は、nohupをしてもプロセスが死ぬ場合があります。これは、SIGCONTSIGTERMSIGHUPではないので受け付けてしまうからです。Runningのプロセスの場合は、これらのシグナルを受信することはありません。

さて、プロセス停止中とはどういう状態でしょうか?

Linux のプロセスのステータス

Linux のプロセスのステータスを調べてみましょう。
* 9. Process States

5つのステータスがあるようです。

  1. R: 実行中もしくは、実行可能。CPUからの割り当てを待っている。
  2. S: インタラクティブスリープ、イベントを待っている。例えばターミナルからの入力待ちなど
  3. D: 非インタラクティブなスリープ。プロセスが殺せなかったり、シグナルで中断されている。大抵はOSのリブートで解消する
  4. Z: ゾンビ、殺されたプロセスで、ステータスが収集されるのを待っている
  5. T: 停止中、プロセスは、停止されていたり、止められている。

これらは、例えば、ps auxSTATのカラムで確認できる。

$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   8936   312 ?        Ssl  Dec13   0:00 /init ro
root         8  0.0  0.0   8944   228 tty1     Ss   Dec13   0:00 /init ro
ushio        9  0.0  0.0  10660   664 tty1     S    Dec13   0:00 sh -c"$VSCODE_WSL_EXT_LOCATION/scrip
ushio       10  0.0  0.0  10660   712 tty1     S    Dec13   0:00 sh /mnt/c/Users/tsushi/.vscode/extens
ushio       15  0.0  0.0  10660   696 tty1     S    Dec13   0:00 sh /home/ushio/.vscode-server/bin/879
ushio       17  0.0  0.1 971068 30456 tty1     Sl   Dec13   0:35 /home/ushio/.vscode-server/bin/8795a9
ushio       32  0.0  0.0 865420 14532 tty1     Sl   Dec13   0:14 /home/ushio/.vscode-server/bin/8795a9
ushio      157  0.0  0.3 1216168 56644 tty1    Rl   Dec13   1:27 /home/ushio/.vscode-server/bin/8795a9
ushio      170  0.0  0.0  20616  4020 pts/0    Ss   Dec13   0:09 /usr/bin/zsh
ushio      307  0.0  0.0 566720  1148 tty1     Sl   Dec13   0:00 /home/ushio/.vscode-server/bin/8795a9
ushio     2058  0.0  0.0  20088  4416 pts/1    Ss   Dec14   0:03 /usr/bin/zsh
ushio     9777  0.0  0.0  17380  1920 pts/1    R    13:35   0:00 ps aux

シグナルの種類

先ほどの2つのシグナルは

  • SIGTERM: 終了シグナル (termination)
  • SIGCONT: 一時停止からの再開
  • SIGHUP: 制御端末のハングアップ検出、もしくは、制御しているプロセスの死

nohupまとめ

ただし、私が意図しているプロセスは、サーバープロセスなので、停止中という状態は何かおかしいことが発生しない限りならないので、基本nohup ... &で、確実に実行可能だと思われます。(すっきり)

リダイレクト

最初の定番のコードの残りはリダイレクトです。 > filename 1<2&をすると、標準出力と、エラー出力を両方ファイルに出力することは知っていますが、文法的にどうなっているのでしょうか?

$ nohup /foo/var.sh > some.log 1<2&

std_in_out.sh

#!/bin/bashecho"stdout">&1
echo"stderr">&2

こんな感じで、標準出力と標準エラー出力を単純にアウトプットするシェルを書いてみます。早速リダイレクトがでてきていますが 、まずは、そういう機能であることだけ理解しましょう。

>

これは、1>が省略された形です。標準出力をファイルに出力します。このリダイレクトの本質は、ファイルディスクリプタの代入です。1のファイルディスクリプタをファイルに変更しているわけです。

$ ./std_in_out.sh 
stdout
stdout
$ ./std_in_out.sh > stdout.log
stderr
# cat stdout.log 
stdout

ですので、結果として、標準出力のみがファイルに出力されて、エラー出力に関してはディスクリプタを変更していないので、そのまま画面に表示されています。

数字>

ファイルディスクリプタを指定してリダイレクトします。

cat stdout.log 1> stdout.log
$ ./std_in_out.sh 1> stdout.log 
stderr
$ cat stdout.log 
stdout
$ ./std_in_out.sh 2> stderr.log
stdout
$ cat stderr.log 
stderr

&>

標準出力と、標準エラー出力を両方ファイルに出力します。

$ ./std_in_out.sh &> stdout_error.log
$ cat stdout_error.log 
stdout
stderr

/dev/null

出力を消去します。次の例では、標準エラー出力だけ消去しています。

bash
$ ./std_in_out.sh 2> /dev/null
stdout
`

複数リダイレクト実行

リダイレクトは順番に実行される。num1>&num2のコマンドは、ファイルディスクリプタのnum2のコピーをnum1に代入するというコマンド。つまり、このリダイレクトは、未定義のディスクリプタ3に標準入力(のコピー、つまり、その後、1が変更されても影響を受けない)が代入される。次に、1に2が代入されて、最終的に3が2に代入される。つまり、標準出力と、標準エラー出力が入れ替わるというものだ。

$ { bash ./std_in_out.sh 3>&1 1>&2 2>&3;} 1> stdout.log 2>stderr.log
$ cat stdout.log
stderr
$ cat stderr.log
stdout

ちなみに、ここで出てきている{ }の記述は複合コマンドという文法で、複数のコマンド実行を一つの標準出力、エラー出力にまとめるというものだ。

num1>&num2のコマンドの動作を確認しよう。ここで、
1. 3>&1 (1: 標準出力 2: 標準エラー出力 3: 標準出力)
2. 1> stdout.log (1: stdout.log 2: 標準エラー出力 3: 標準出力)
3. 2>&3 (1: stdout.log 2: 標準出力 3: 標準出力)

つまり、シェル内の標準出力は、stdout.logに書かれるが、エラー出力はそのまま標準出力に出力される。3に代入した、標準出力はコピーであり、1自体もしくは、ポインタではないので、1がのちに変更されても、その値は影響を受けない。

$ ./std_in_out.sh 3>&1 1> stdout.log 2>&3
stderr
$ cat stdout.log 
stdout

ユーザー指定のコマンド実行

$
0
0

自動化のスクリプトで、ユーザ指定でスクリプトを実行したい場合はどうしたらいいだろう?自動化なので、インタラクティブがあってはいけない。

どうやら普通にsudoを使えばよいらしい。

sudo--helpsudo - execute a command as another user

usage: sudo-h | -K | -k | -V
usage: sudo-v[-AknS][-g group] [-h host] [-p prompt] [-u user]
usage: sudo-l[-AknS][-g group] [-h host] [-p prompt] [-U user] [-u user] [command]
usage: sudo[-AbEHknPS][-r role] [-ttype][-C num] [-g group] [-h host] [-p prompt] [-Ttimeout][-u user] [VAR=value] [-i|-s] [<command>]
usage: sudo-e[-AknS][-r role] [-ttype][-C num] [-g group] [-h host] [-p prompt] [-Ttimeout][-u user] file ...

Options:
  -A, --askpass                 use a helper program for password prompting
  -b, --background              run command in the background
  -C, --close-from=num          close all file descriptors >= num
  -E, --preserve-env            preserve user environment when running command--preserve-env=list       preserve specific environment variables
  -e, --edit                    edit files instead of running a command-g, --group=group             run command as the specified group name or ID
  -H, --set-homeset HOME variable to target user's home dir
  -h, --help                    display help message and exit
  -h, --host=host               run command on host (if supported by plugin)
  -i, --login                   run login shell as the target user; a command may also be specified
  -K, --remove-timestamp        remove timestamp file completely
  -k, --reset-timestamp         invalidate timestamp file
  -l, --list                    list user's privileges or check a specific command; use twice for longer format
  -n, --non-interactive         non-interactive mode, no prompts are used
  -P, --preserve-groups         preserve group vector instead of setting to target's
  -p, --prompt=prompt           use the specified password prompt
  -r, --role=role               create SELinux security context with specified role
  -S, --stdin                   read password from standard input
  -s, --shell                   run shell as the target user; a command may also be specified
  -t, --type=type               create SELinux security context with specified type
  -T, --command-timeout=timeout terminate command after the specified time limit
  -U, --other-user=user         in list mode, display privileges for user
  -u, --user=user               run command (or edit file) as specified user name or ID
  -V, --version                 display version information and exit
  -v, --validate                update user's timestamp without running a command--                            stop processing command line arguments

確かにそもそもsudo はそのためのコマンドだ。普段は、sudoが使えるユーザが、Root権限がいるときに使っていたが、そうでないケース、今回だと、rootで実行しているスクリプトをazureuserで動かしたい。chmodとかもしてもいいけど、面倒だ。

これに従うと-uのフラグで行けそうだ。

sudo-u hoge cat sample.txt
Password:

そうだ、普段はパスワードを聞かれる。rootから他のユーザに移るときはどうだろう?パスワード必要なのはつらい。

$ sudo su
[sudo] password for ushio: 
# sudo -u ushio ./std_in_out.sh 
stdout
stderr

うむ。rootからだと、パスワードは不要らしい。じゃあ、完全に自動化できるので、問題なし!

Shellとは

$
0
0

Shellに関する基本的な知識をまとめていきます。

Shellとは

シェルはOSの中核であるカーネルと対話するためソフトウェアのことです。表現を変えれば、人間とPC(カーネル)は直接やりとりができないのでシェルがやり取りの仲介役となってくれているというもの。

ターミナル・コンソール

ターミナル、コンソールはどれもCUIでLinuxにコマンドを打ち込むときに使用するものです。これら二つはシェルと同じであるとよく間違えられますが厳密には違う意味を表しています。それぞれ一つずつ説明します。

コンソール
現時点で「コンソール」というと一般的に黒い画面でコマンド入力を受け付ける画面を指すことが多いです。

ターミナル
コマンド入力を受け付ける点はコンソールと同じですが、GUIの上でCUIの操作をしたいときに使用するアプリケーションという点でコンソールとは異なります。

コンソール&ターミナルでコマンド入力を受け付け、受けた入力をシェルが仲介して、PC(カーネル)に分かるようにしているイメージ。

BashとZsh

BashもZshもシェルの一種を表します。
基本的なシェルとしてBashが多く導入されていますが、Zshには以下のような導入メリットがあります。

Zsh

・カスタマイズができる
・他のシェルの機能を取り込んでおり、性能が高い
・軽量
・ユーザーの起動している全てのzshでコマンド履歴を共有することができる。
・プログラム可能な補完機能に拠って、多くのユーザーコマンドのオプションや引数を打つのを支援する

これだけメリットがあるとZsh導入した方が良さそうですね…!

Tmuxとは

tmuxは端末多重化ソフトウェアです。詳しく言うと1つのターミナル上で複数のターミナルを立ち上げて同時並行で作業できるものです。つのターミナルで操作しているシェルから実行したtmux上で複数の仮想端末を操作できるため、ターミナルを複数開くことなく複数のサーバへログインしたり、それぞれの仮想端末で別々のプログラムを実行できるようになるため、より効率的に作業が行えます。スクリーンショット 2019-12-16 15.20.51.png


踊るgifシェル芸

$
0
0

踊るgifシェル芸とは?

x.gif
こういうgifアニメーションをつくるシェル芸です。
このシェル芸はいらすとやの素材を使用しています。

素材とコマンド

以下の素材とコマンドで、gifアニメーションを作ることができます。
man.png
man.png
woman.png
woman.png
コマンド

$ convert -dispose previous man.png \( woman.png -flop\) +append -write mpr:a +delete \( man.png -flop\) woman.png +append -write mpr:b +delete mpr:a mpr:a mpr:a \( mpr:b -scale x90% \) mpr:b mpr:b mpr:b \( mpr:a -scale x90% \) /images/x.gif

前提条件はimagemagickがインストールされていること。

コマンドができるまで

コマンドができるまでを見ていきましょう。

ステップ1. 反転してアニメーションする

まず一人の人を反転してアニメにしてみます。
コマンド

$ convert man.png \( +clone -flop\) /images/0.gif

出力アニメ
0.gif
2コマのアニメにはなりましたが、素材が透過pngなので、1コマめが残っちゃってますね。
-dispose previousで消しましょう。
コマンド

$ convert -dispose previous man.png \( +clone -flop\) /images/1.gif

出力アニメ
1.gif
消えました。

ステップ2. 弾ませる

左右反転するときに上下に弾むようにしましょう。
-scale x90%で縦に少し縮めたフレームを作って、反転時に挿入します。
コマンド

$ convert -dispose previous man.png \( man.png -flop-scale x90% \)\( man.png -flop\)\( man.png -scale x90% \) /images/2.gif

出力アニメ
2.gif

ステップ3. 接地時間調整

地に足がついている時間を長めにしましょう。
元画像を-write mpr:aでmpr:aに格納、反転画像を-write mpr:bでmpr:bに格納して、
mpr:a を3コマ続けて、mpr:b を縦に縮めたものを挟んで、
mpr:b を3コマ続けて、mpr:a を縦に縮めたものを挟んで、もとの mpr:a にもどります。
コマンド

$ convert -dispose previous man.png -write mpr:a +delete \( mpr:a -flop\)-write mpr:b +delete mpr:a mpr:a mpr:a \( mpr:b -scale x90% \) mpr:b mpr:b mpr:b \( mpr:a -scale x90% \) /images/3.gif

出力アニメ
3.gif

ステップ4. 2人にする

+appendで横に結合したものを踊らせます。
コマンド

$ convert -dispose previous man.png \( woman.png -flop\) +append -write mpr:a +delete \( man.png -flop\) woman.png +append -write mpr:b +delete mpr:a mpr:a mpr:a \( mpr:b -scale x90% \) mpr:b mpr:b mpr:b \( mpr:a -scale x90% \) /images/x.gif

出力アニメ
x.gif
完成です。

シェル芸botの場合

シェル芸botでは、入力ツイートでアップロードした画像が /media/0, /media/1,… のパスにあるものとして
扱われるので、以下のようになります。
入力ツイート


出力ツイート

応用

反転時に上に弾むのではなく、下に沈むのでも踊らせることができます。
コマンド

$ echo-e"\n💃 "|textimg -F100|convert -dispose previous - -trim +repage -scale 72x100! -write mpr:a -flop-write mpr:b +delete -size 72x10 xc:black \( mpr:b -scale x90% \)-append-write mpr:c +delete mpr:a mpr:a mpr:a mpr:c mpr:b mpr:b mpr:b \( mpr:c -flop\) /images/d.gif

出力アニメ
d.gif
応用というか引き算ですが、上下に弾ませなくてもけっこう面白いものはできます。



まとめ

画像を左右反転して、上下に弾ませることで、踊るgifを作ることができました。

所感

阿波踊りのやつもシェル芸botでやったと思うんですが、ちょっと見つかりませんでした。
見つけたら追記します。

ソフトウェアツールとしてはimagemagick一発実行してるだけなので、
シェル芸としては微妙かもしれないですね。
imagemagickの各種コマンドラインツール
パイプやリダイレクトでつなぐのをそのうちやろうかな。

ターミナルをハックする方法

$
0
0

フューチャー Advent Calendar 2019 17日目の記事です。

最近は Go にはまっていますが、今回は趣向を変えてみます。前のプロジェクトでたまに使っていた、ターミナルを共有する ttycopyというツールの紹介をしようと思います。

スーパーエンジニアがサーバ上に ssh してなんやかんやしているときに、ちょっと画面を ハックしたい見たい ...! みたいなシーンがあります。あとは、サーバ上でコマンドが正しく入力されているかとかのダブルチェックとかで画面を確認するとか。物理的な距離があったりすると、モニタに画面を映すことも難しいですよね。1

画面をみせてもらうのも一つの方法ですが、ちょっとしたツールで参照できるとスマートですよね。Perl だと ttylogというツールがあったりします。私は ttylog にインスパイアされて、シェルで動く ttycopyを作りました。シェルで動作するので ttycopy の実装をコピー&ペーストしてカジュアルに動かすこともできます。

サンプル動作

以下のように動作をします。ちょっとわかりにくいですが、あるサーバに root ユーザと tsuji ユーザでログインしていて root ユーザで ttycopy を実行しています。ターミナルで途中から tsuji ユーザでログインしているターミナルが表示されていることが分かります。

ttycopy

ttylog がどのように動作しているかは、ターミナル画面を勝手に共有して他人の作業を覗いてみるなどでも紹介されていますが、基本的な処理の流れは以下のとおりです。

  1. tty のログインプロセスの PID を調べる
  2. PID に対し strace コマンドを使ってシステムコールをのぞき見る
  3. strace で出力される read システムコールの文字列を整形して表示する

ttycopy では上記の手順に沿ってシェルで実装しました。

シェル(Bash)での実装

1.tty のログインプロセスの PID を調べる

シェルでの実装は以下です。

pid=`ps fauwwx | grep sshd.*${tty} | grep-vgrep | sed-e's/^[a-zA-Z0-9]\+[ \n\r\f\t]\+\([0-9]\+\).*/\1/'`

これは grep したときに取得できる以下のようなプロセスツリーをの文字列を正規表現を用いて PID をキャプチャしています。正規表現でスッキリかけますね。

[tsuji@localhost ~]$ ps fauwwx | grep sshd.
root      1531  0.0  0.1  82568  6236 ?        Ss   15:29   0:00 /usr/sbin/sshd -D
root      2830  0.0  0.2 149824  8916 ?        Ss   15:29   0:01  \_ sshd: root@pts/0
root     13315  0.0  0.2 158980 10280 ?        Ss   18:58   0:00  \_ sshd: root@notty
root     14956  0.0  0.2 154512  9352 ?        Ss   20:10   0:00  \_ sshd: tsuji [priv]
tsuji    14959  0.0  0.1 154512  4092 ?        S    20:10   0:00      \_ sshd: tsuji@pts/1
tsuji    15012  0.0  0.0 112672  2268 pts/1    S+   20:11   0:00              \_ grep --color=auto sshd.

2.PID に対し strace コマンドを使ってシステムコールをのぞき見る

シェルでの実装は以下の部分です。

strace -eread-s16384-q-xx-p${pid} 2>&1

やっていることは straceを用いて、先程取得した tty に紐づくPID が発行する read システムコールを取得します。read システムコール以外にもいろいろなシステムコールが呼び出されるため、strace のオプション -e readで read システムコールのみ抽出しています。

read システムコールは以下で定義されているシステムコールでした。strace で取得できる結果も以下のようになっています。

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

例えば参照先のターミナルで以下のようなコマンドを発行したとします。

  • 取得元のターミナル
[tsuji@localhost gomi]$ hoge
bash: hoge: コマンドが見つかりませんでした...
[tsuji@localhost gomi]$

このとき strace -p ${PID} -e readで read システムコールを参照すると以下のように出力されます。

  • strace しているターミナル
[root@localhost ttycopy]# strace -p 14959 -e read
strace: Process 14959 attached
read(3, "\0\0\0\20\356\202\375C&&\357q\276\210pZ)\300\26M\357T\313\303k\6p\232\351\263\32\224"..., 16384) = 36
read(11, "h", 16384)                    = 1
read(3, "\0\0\0\20\235\230\204Ud\36)\370\266\233\362\305\2219\253g\335M\23\212\374h\250i@\235/\216"..., 16384) = 36
read(11, "o", 16384)                    = 1
read(3, "\0\0\0\20\324\357\304\vn\357BbW\241m\220yS\236\362\301\337\337\237c\203\245\223\221\253;,"..., 16384) = 36
read(11, "g", 16384)                    = 1
read(3, "\0\0\0\20\344\215\235\300\226\236\0\323\376\r\217,\257\322\326w\323R\264\3}\266\7q\315\215\344\346"..., 16384) = 36
read(11, "e", 16384)                    = 1
read(3, "\0\0\0\20^``\333\263h\372Z\336\335Y2\250\203\335\221\372faj\177\260f\302Sb\35\354"..., 16384) = 36
read(11, "\r\n", 16384)                 = 2
read(11, "bash: hoge: \343\202\263\343\203\236\343\203\263\343\203\211\343\201\214\350\246\213\343\201"..., 16384) = 62
read(11, "\33]0;tsuji@localhost:~/gomi\7[tsuj"..., 16384) = 51
^Cstrace: Process 14959 detached

上の例だと strace: Process 14959 attachedstrace: Process 14959 detachedという文字列が表示されていますが、この出力を抑制するために strace のオプション -qを用いています。また、表示するときに扱いやすいように -xオプションで文字列を 16 進数に変換しています。

3.strace で出力される read システムコールの文字列を整形して表示する

目標は 2 の手順で取得した read システムコールの第 2 引数の内容を表示させることです。シェルでの実装は以下です。

sed-une"s/^read([0-9]\+, \"\(.*\)\".*/echo -n \$'\1'/p" | bash

まず前半部分 sed -une "s/^read([0-9]\+, \"\(.*\)\".*/echo -n \$'\1'/p"です。正規表現で read システムコールの第 2 引数をキャプチャして、それに対して echo -n$''の文字列を付与しています。よって以下のような文字列を取得できます。

  • 取得元のターミナルに出力されている文字列
[tsuji@localhost gomi]$ cat hoge
hoge
[tsuji@localhost gomi]$
  • strace して加工後に出力されるターミナルの文字列
echo -n $'\x00\x00\x00\x10\xc6\x1e\x56\xc6\x1f\x11\x7e\x57\x11\xaf\xdb\x2a\x91\x32\x84\x8e\x6e\x5b\x12\xc1\x72\x94\x36\x17\x12\xbb\x7c\xab\x4b\xdd\x19\x33'
echo -n $'c'
echo -n $'\x00\x00\x00\x10\x5d\x68\x72\x7c\x74\xca\x3c\xd1\x57\xfc\x14\x7d\x55\x34\x66\x15\x03\xcb\x26\x7c\x17\xbc\x7f\x7a\xf5\x25\x40\xed\xa8\x21\x39\xb3'
echo -n $'a'
echo -n $'\x00\x00\x00\x10\x05\x3d\x4a\xc2\x76\x1c\xd4\x23\x2a\x17\xc6\xa1\x1c\xf2\xdb\x14\x75\x1c\x7d\xb7\x21\xfb\xfc\xcd\x2d\x5c\xef\x06\x6c\x97\x01\x28'
echo -n $'t'
echo -n $'\x00\x00\x00\x10\x66\xb0\x8c\x40\x10\xa6\xf3\x9b\x36\x75\xd5\xc1\x65\x63\x94\x4f\x77\xd9\x10\x6d\xcf\xbb\x48\xed\x8b\x43\x58\x20\x54\x08\xde\x9b'
echo -n $' '
echo -n $'\x00\x00\x00\x10\x60\x6e\xb6\x06\x43\x16\xf5\x75\x89\x90\xb6\x42\x2c\xfe\x8b\x97\xae\xad\x47\x26\xf9\x39\xfc\xd2\x84\x37\xde\x0d\xe5\x32\xbc\x80'
echo -n $'h'
echo -n $'\x00\x00\x00\x10\x00\xe4\x3d\xb7\xd9\x79\x2e\x46\x80\xd5\xa5\xc2\xa7\x9a\xc7\x0c\xe1\x58\x7b\xd5\x97\xff\x00\xab\x72\x51\xa4\xbb\xab\x7d\xd1\xaf'
echo -n $'o'
echo -n $'\x00\x00\x00\x10\xf8\x1d\xce\xe6\x7f\x1a\x43\x94\xa2\xde\x3d\x3c\xb5\xe9\xb9\x94\x39\x43\x63\xfd\xa9\x1f\x45\x83\x64\x5c\x3a\xdf\xa8\x1a\xa4\x86'
echo -n $'g'
echo -n $'\x00\x00\x00\x10\x90\x69\x42\xa9\x48\x99\x0c\x52\xe9\x49\xbd\x4e\xa5\x17\x01\xff\xac\xec\x29\x75\x2c\xc0\x2b\x7c\x07\x85\xf2\x2f\xce\x71\x8f\x46'
echo -n $'e'
echo -n $'\x00\x00\x00\x10\xd9\x91\x38\x08\x0b\x95\x78\xd4\x80\x51\xa2\xe8\xef\x20\x06\x45\xa9\x3c\xf0\xa3\x10\x7d\x06\x32\x2d\x31\x53\x57\x40\x77\x1b\x29'
echo -n $'\r\n'
echo -n $'\x68\x6f\x67\x65\x0d\x0a\x1b\x5d\x30\x3b\x74\x73\x75\x6a\x69\x40\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x3a\x7e\x2f\x67\x6f\x6d\x69\x07'
echo -n $'[tsuji@localhost gomi]$ '

echo -n-nオプションは解釈する際に改行されないようするためです。Bash の $''の形式は以下の効果があります。Man page からの抜粋です。2

$'string' の形式を持つ単語は特殊な扱いを受けます。 この単語は string に展開され、 それから ANSI C 標準で仕様が決められている、 バックスラッシュでエスケープされている文字に置き換えられます。 バックスラッシュエスケープシーケンスは、 (もし存在すれば) 以下のようにデコードされます:

これを用いることで、文字列が保持している意味に展開することができます。

あとは | bashechoの文字列をシェルに解釈させることで、文字列を表示することができます。

まとめ

strace で read システムコールの引数の文字列を取得し、リアルタイムに復元することでターミナルをカジュアルに共有することができました。他の方の作業をリアルタイムで見れるので教育的なツールではないかと思います。ターミナルをハックしてみたい方はぜひ使ってみてください。

よかったら ttycopyに star もつけてもらえるとモチベーションが上がります:v:


  1. もちろん、使うときは断ってから使いましょう。 

  2. https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html 

bashのプロンプト表示を管理する便利ツールを作ったので紹介する

$
0
0

この記事は dotfiles Advent Calendar 2019 17日目の記事です。1

今回は、bashのプロンプト表示を簡単に管理する便利ツール(エイリアス群)、
promptrcについて紹介します。

promptrcの紹介

導入方法

promptrcのGithubリンク

コードとしては以下となります。

# 必要な変数定義部分export BLACK="$(tput setaf 0)"export RED="$(tput setaf 1)"export GREEN="$(tput setaf 2)"export YELLOW="$(tput setaf 3)"export BLUE="$(tput setaf 4)"export MAGENTA="$(tput setaf 5)"export CYAN="$(tput setaf 6)"export WHITE="$(tput setaf 6)"export RESET="$(tput sgr0)"# プロンプト表示設定部分export PS1='[\A ${GREEN}\u${RESET}${CYAN}@\H${RESET} \W]\n\$ '

sourceコマンドで読み込むと反映されます。

$ source promptrc

.bashrcにsourceコマンドを記載しておくことで、ログインするたび読み込むことが可能です。

# bashrcに追記
source ~/promptrc

dotfiler2の方は、bashrcに読み込む旨を追記してください。
で、promptrcをadd, commit, pushして、Gitで管理してあげてください。

# bashrcに追記source ~/dotfiles/promptrc

デフォルトだと、プロンプト表示は以下のようになります(私のお気に入りの表示です)。
image.png

利用方法

色のカスタマイズ

色を変更したい場合は、${COLOR}で指定します。
試しに、デフォルトを以下のように書き換えてみましょう。
書き換えたらsourceで再読み込みしてください。

- export PS1='[\A ${GREEN}\u${RESET}${CYAN}@\H${RESET} \W]\n\$ '
+ export PS1='[\A ${RED}\u${RESET}${YELLOW}@\H${RESET} \W]\n\$ '

なお、とりあえず一時的に変更したい場合は、以下のように環境変数を設定しなおすだけで問題ありません(動作確認など)

$ PS1='[\A ${RED}\u${RESET}${YELLOW}@\H${RESET} \W]\n\$ '

以下のように表示が変更されました。
image.png

表示のカスタマイズ

表示内容についても同様に変更できます。パラメータについてはこちらのブログが詳しいです。
以下引用します。

\aベル(ビープ音)をならします(ASCII のベル文字 07)
\d"曜日 月 日"の形式の日付
\hホスト名(最初の . までの名前)
\Hホスト名
\n改行
\r復帰
\sシェル名(標準だと -bash が表示)
\t時刻 HH:MM:SS 形式(24時間) H = Hour = 時、M = Minutes = 分、S = Seconds = 秒
\T時刻 HH:MM:SS 形式(12時間)
\@時刻 am/pm をつけたもの。Lang=Jaの場合 HH:MM (午前
\u現在のユーザー名
\vbash のバージョン
\Vbash のバージョン・リリース番号など詳細
\w現在のディレクトリ(フルパス)
\W現在のディレクトリ名
!コマンドのヒストリー番号
#コマンドのコマンド番号(ログイン後何回実行したか)
\$UID が 0 であれば『 # 』、それ以外は『 $ 』
\バックスラッシュ
[表示されない文字列(エスケープシーケンス/端末制御シーケンス)を埋め込む
]表示されない文字列の終わり

では出力内容についてカスタマイズしてみましょう。
日付出力 + 時刻について詳細に表示するようカスタマイズを行います。

- export PS1='[\A ${RED}\u${RESET}${YELLOW}@\H${RESET} \W]\n\$ '
+ export PS1='[\d \t ${RED}\u${RESET}${YELLOW}@\H${RESET} \W]\n\$ '

image.png

dateコマンドの結果をそのまま出力することも可能です。
自作コマンド等の出力を利用することももちろん可能です。

- export PS1='[\d \t ${RED}\u${RESET}${YELLOW}@\H${RESET} \W]\n\$ '
+ export PS1='[`date`${RED}\u${RESET}${YELLOW}@\H${RESET} \W]\n\$ '

image.png

何がよいのか

やっていることとしては、色のパラメータを変数にまとめただけです。
\[\033[31m\]等のパラメータを直書きしている記事が多く、最初は私もそれでやってましたが
変更の際いちいちググる必要があり面倒でした。

変数で指定するようなものが見当たらなかったため自作しました。

まとめ

  • プロンプトのカスタマイズは気持ちいい
  • \[\033[31m\]とかで指定すると面倒なので、変数にまとめておこう。
  • 各々が調べて車輪の再発明するのはアレなので、この機会に公開しよう!

おわりに

dotfiles Advent Calendar 2019に参加させていただき、ありがとうございました。

キーボードやマウス、端末にこだわる方は多くても、プロンプトにこだわる方は少なかったりするのではないでしょうか?

一般に道具に拘ると愛着が増して、使うのが楽しくなってくるものと思います。
プロンプトもカスタマイズすると愛着が沸いてかわいく思えてきます。

ターミナルでの作業が楽しくなるので、未カスタマイズの方はぜひカスタマイズしてみてください。
紹介したツールを導入していただければ、カスタマイズおよび管理のハードルが下がると思います。

次回は18日目、@ahuglajbclajepさんです。

補足

  • EZPROMPTという便利なツールがありました。(Qiitaの紹介記事はこちら)

  • こんなネタプロンプトの作成も可能です。
    image.png

  • PS1=''とか発行するとかなり異様な操作感が味わえます。元の世界に帰りたい場合はexitを発行してください。


  1. 一度言ってみたかったんですよねこれ 

  2. 正式な呼び方を存じておりません。界隈標準の呼称とかあるんでしょうか 

GAS+slack+GitLab Runners API+bash+LaunchtclでGitLab Runnerの監視プログラムを組む

$
0
0

GitLab Runnerが音もなく死んでいる(offline状態?)になっていることがたびたび起きたので、定期的にRunnerの状態をチェックして死んでたら教えてくれるslack appを作成した。

前提条件

GitLabはアクセスは社内LANからの物しか受け付けないようになっている。
したがってRunnerの状態のチェックだけは必ず社内のPCからしなくてはならない。

使用機材

iMac (OS ver : macOS mojave 10.14.4)

ざっくりとした構成

GAS : 監視対象のRunner一覧をspreadSheetで記録。スクリプトをウェブアプリケーションとして公開しておき、httpアクセスで追加・削除・登録されているもの一覧の確認をできるようにする

bash : GASから監視対象一覧を取得。ひとつひとつRunners APIをたたいてRunnerのstateを確認。死んでたらそれぞれのRunnerの管理人にslackでメッセージを送信。

mac : 上記のbashスクリプトをlaunchctlコマンドで15分ごとに定期実行

slack : appを作成。Runnerが死んでいたらbashがこのappとしてそのRunnerの管理人にメッセージを送る。また、スラッシュコマンドでGASにアクセスしてRunnerの追加・削除・登録されているもの一覧の取得ができるようにする。

それぞれのコード

GAS

A1に「id」B1に「管理人」とそれぞれ項目名を書いたspreadSheetを用意し、以下のようなスクリプトを作成し、ウェブアプリケーションとして公開。「管理人」はそのidを監視対象に追加した人と判断する。

// スラッシュコマンド用// eとしては以下のような形のjsonが来る。//{//    "parameter": {//        "channel_name": "hogehoge",//        "user_id": "A0B1C2DE",//        "user_name": "kkkkan",//        "trigger_id": "012345678.abcdefg",//        "team_domain": "××-jp",//        "team_id": "ABCABC012012",//        "text": "0",//        "channel_id": "AAAAAAA",//        "command": "/add_id",//        "token": "abc456DEF0123",//        "response_url": "https://hooks.slack.com/commands/××/●●/▼▼"//    },//    "contextPath": "",//    "contentLength": 100,//    "queryString": "",//    "parameters": {//        "channel_name": [//            "hogehoge"//        ],//        "user_id": [//            "A0B1C2DE"//        ],//        "user_name": [//            "kkkkan"//        ],//        "trigger_id": [//            "012345678.abcdefg"//        ],//        "team_domain": [//            "××-jp"//        ],//        "team_id": [//            "ABCABC012012"//        ],//        "text": [//            "0"//        ],//        "channel_id": [//            "AAAAAAA"//        ],//        "command": [//            "/add_id"//        ],//        "token": [//            "abc456DEF0123"//        ],//        "response_url": [//            "https://hooks.slack.com/commands/××/●●/▼▼"//        ]//    },//    "postData": {//        "type": "application/x-www-form-urlencoded",//        "length": 357,//        "contents": "token=abc456DEF0123&team_id=ABCABC012012&team_domain=××-jp&channel_id=AAAAAAA&channel_name=hogehoge&user_id=U4TCA3K8S&user_name=A0B1C2DE&command=%2Fadd_id&text=0&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2××%2●●%2▼▼&trigger_id=ABCABC012012",//        "name": "postData"//    }//}functiondoPost(e){// jsonをdecodevarjson_str=JSON.stringify(e,null,"\t");//  return ContentService.createTextOutput(json_str).setMimeType(ContentService.MimeType.JSON);// jsonを再度encodevarjson=JSON.parse(json_str);// コマンドを打った人varuser_name=json.parameter.user_name;// コマンドに渡した文字列vartexts=json.parameter.text.split('');// コマンド名varcommand=json.parameter.command;// spread sheetvarspreadsheet=SpreadsheetApp.openById('【spreadSheetのID】');// 1枚目のシートvarsheet=spreadsheet.getSheets()[0];varbody="";if(command=="/add_runner"){// 追加コマンドだったらvarid=texts[0];if(texts.length==1&&id!=null&&id!=''&&!isNaN(id)){// コマンドが受け付けるメッセージは1つの数字だけvarstartrow=2;varstartcol=1;varlastrow=sheet.getLastRow();varlastcol=sheet.getLastColumn();//がさっと取得varsheetdata=sheet.getSheetValues(startrow,startcol,lastrow,lastcol);for(vari=0;i<lastrow-1;i++){if(sheetdata[i][0]==id&&sheetdata[i][1]==user_name){// もしすでにidも管理者も同じRunnerが追加済みだったらbody=body+user_name+"の管理する Id "+texts[0]+"のRunnerは既に監視対象です。\n/runner_listコマンドで現在登録されているRunnerの一覧が確認できます。";}}if(body==""){// 新規追加// 監視対象に追加vararrData=sheet.getDataRange().getValues();arrData.push([id,user_name]);varrows=arrData.length;varcols=arrData[0].length;sheet.getRange(1,1,rows,cols).setValues(arrData);// 表示するメッセージbody=body+"Runner Id "+texts[0]+"を監視対象に追加しました。";}}else{// メッセージ内容が間違っていたらbody=body+"メッセージが間違っています。/add_runner [Runner Id] で監視対象に追加できます。";}}elseif(command=="/runner_list"){// 登録してあるreunner一覧表示コマンドだったらvarlist_str=getList();// 見やすいよう整形body=JSON.stringify(JSON.parse(list_str),null,"\t");}elseif(command=="/remove_runner"){// 削除コマンドだったらvarid=texts[0];if(texts.length==1&&id!=null&&id!=''&&!isNaN(id)){// コマンドが受け付けるメッセージは1つの数字だけvarstartrow=2;varstartcol=1;varlastrow=sheet.getLastRow();varlastcol=sheet.getLastColumn();//がさっと取得varsheetdata=sheet.getSheetValues(startrow,startcol,lastrow,lastcol);varbody="";for(vari=0;i<lastrow-1;i++){if(sheetdata[i][0]==id&&sheetdata[i][1]==user_name){// そのidの、その人が管理するrunnerが監視対象に含まれていたら// 削除sheet.getRange(startrow+i,1,1,2).clear();// 空白になってしまった行を削除sheet.deleteRow(startrow+i)// 表示するメッセージbody=body+user_name+"の管理する Id "+texts[0]+"のRunnerを監視対象から削除しました。";}}// 表示するメッセージ// もし何も削除しなかったらif(body==""){body=body+user_name+"の管理する Id "+texts[0]+"のRunnerは監視対象に含まれていませんでした。\n/runner_listコマンドで現在登録されているRunnerの一覧が確認できます。";}}else{// メッセージ内容が間違っていたらbody=body+"メッセージが間違っています。/remove_runner [Runner Id] で監視対象から削除できます。";}}else{body=body+"false";}returnContentService.createTextOutput(body).setMimeType(ContentService.MimeType.JSON);}// 監視スクリプトが呼ぶ用のメソッド// 監視するべきrunnerのidと管理人を返すfunctiondoGet(){returnContentService.createTextOutput(getList()).setMimeType(ContentService.MimeType.JSON);}// 監視するべきrunnerのidと管理人を返すfunctiongetList(){// spread sheetvarspreadsheet=SpreadsheetApp.openById('【spreadSheetのID】');// 1枚目のシートvarsheet=spreadsheet.getSheets()[0];varstartrow=2;varstartcol=1;varlastrow=sheet.getLastRow();varlastcol=sheet.getLastColumn();//がさっと取得varsheetdata=sheet.getSheetValues(startrow,startcol,lastrow,lastcol);varbody="[";for(vari=0;i<lastrow-1;i++){body=body+"{ \"id\":\""+sheetdata[i][0]+"\", \"admin\":\""+sheetdata[i][1]+"\"}";if(i!=lastrow-2){body=body+",";}}// 返すデータがないもないときにも空配列を返せるように]はfor文の外で。body=body+"]";Logger.log(body);returnbody;}

bashスクリプト

check_gitlab_runners.sh
GitLabのRunners API(https://docs.gitlab.com/ee/api/runners.html)とSlackのmessage API(https://api.slack.com/methods/chat.postMessage)を使用する。

#!/usr/bin/env bashcd /Users/kkkkan/Desktop/gitlab_runner_watcher/
# 監視するgitlab runnerのidと管理人の配列を取得check_list=$(curl -L【公開したGASウェブアプリケーションのURL】 | jq )list_len=$(echo$check_list | jq length)# tokenと投稿するchannelslack_token="【xoxb-から始まる、slack appのOAuth Access Token】"at_mark="@"# 監視する状態offline='"offline"'paused='"paused"'#echo ${status}#set -xfor i in$(seq 0 $(($list_len-1)))do
    id=$(echo$check_list | jq -r .[$i].id)admin=$(echo$check_list | jq -r .[$i].admin)#for id in "${check_runner_ids[@]}"#doecho${id}http_result=$(curl -s--header"PRIVATE-TOKEN: 【GitLabのtoken】""http://【監視したいGitLabインスタンスのドメイン】/api/v4/runners/${id}" | jq )#echo "$http_result"touch result.json
    echo"$http_result"> result.json
    #cat result.json# messageを取得# 権限などで正常に取れない場合だけmessgeがあるっぽいmessage=$(jq ".message" result.json)#set -x#echo "$message"if["$message"!="null"]then
      payload="GitLab Runner $_runner_name (ID : $id)の状態は取得できません。/remove_runnerコマンドで監視対象から外すか、このRunnerをkkkkanがみれるようにGitLab上から権限を変更してください。"
      curl -X POST -d"token="$slack_token"&channel="$at_mark""$admin"&text=""$payload""https://slack.com/api/chat.postMessage"fi# statusを取得status=$(jq ".status" result.json)_runner_name=$(jq ".description" result.json)runner_name=$(eval echo${_runner_name})if["$status"="$offline"]then#echo "offline$id"payload="GitLab Runner $_runner_name (ID : $id)のステータスがofflineになっています。"#set -x# Runnerの管理人にメッセージをポスト
       curl -X POST -d"token="$slack_token"&channel="$at_mark""$admin"&text=""$payload""https://slack.com/api/chat.postMessage"fi

    if["$status"="$paused"]then#echo "pause$id"payload="GitLab Runner $_runner_name (ID : $id)のステータスがpauseになっています。"#set -x# Runnerの管理人にメッセージをポスト
       curl -X POST -d"token="$slack_token"&channel="$at_mark""$admin"&text=""$payload""https://slack.com/api/chat.postMessage"fi
done

Launchctl

launchctlのplistファイル。~/Library/LaunchAgents/以下に置く。(15分ごとに実行するようにしている。)

gitlabrunner.watcher.plist
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plistversion="1.0"><dict><key>Label</key><string>gitlabrunner.watcher</string><!--フルパスで指定--><key>ProgramArguments</key><array><string>/Users/kkkkan/Desktop/gitlab_runner_watcher/check_gitlab_runners.sh</string></array><key>EnvironmentVariables</key><dict><key>PATH</key><string>/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:/usr/local/sbin</string></dict><key>StandardErrorPath</key><string>/Users/kkkkan/Desktop/error.log</string><key>StandardOutPath</key><string>/Users/kkkkan/Desktop/out.log</string><key>StartInterval</key><!--15分毎--><integer>900</integer><key>RunAtLoad</key><true/><key>ExitTimeout</key><integer>300</integer></dict></plist>

slack

https://api.slack.com/apps
から新しくappを作って、slash commandsを導入し、/add_runner,/remove_runner,/runner_listコマンドを作成。全てRequest URLは先ほど作って公開したGAS webアプリケーションのURLにする。

ハマったところ

  • launchtclが全然実行されない! →
<key>EnvironmentVariables</key><dict><key>PATH</key><string>/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:/usr/local/sbin</string></dict>

最初plistにこれを追加していなかったのだが、そうしたらpathが一部上手く通っていなかったみたいでjqコマンドが見つからなくてエラーしていた。今回ここが一番ドはまりして時間がかかった。

感想など

  • GitLab Runnerが死んだら通知が来るような機構はGitLab CIが備えていても良いようなものだが、見つけられなかったので自前で実装した。が、自分の探し方が下手で見つけられなかっただけの可能性が多分にあるのでもし見つけた方がいたら教えてほしいです。
  • 前提条件に書いたように、GitLabアクセスが社内PCからしかできない環境のためこの構成にしたが、その制約がないならslackへのメッセージ送信もGASスクリプト内で行い、GASウェブアプリケーションの定期実行機能を用いることでGAS+slack+GitLab Runners APIだけで十分実現可能だと思う。

参考にしたもの

SSL証明書の署名アルゴリズムを調べる

$
0
0

日本政府が、2019年度末で使用停止としているSHA-1署名のSSL証明書。

自分が使っている証明書が、SHA-2(sha256)となっているか確認するコマンドを調べた。

bash
$ openssl s_client -connect domain.com:443 -showcerts< /dev/null | openssl x509 -text-in /dev/stdin    

このコマンドで得た出力の中に、以下の記述があればSHA-2であると考えていい。

output
...
Certificate:
  Data:
    ...
  Signature Algorithm: sha256WithRSAEncryption
    ...
...

ブラウザ上で調べる方法もありますが、黒い画面に向かう人間からすると、コマンドのほうが安心感がある。

Viewing all 2850 articles
Browse latest View live