オープンソースこねこね

Webプログラミングなどについてあれこれ。

コマンド実行に関して色々面倒を見る、コマンドラッパーを実装した

github.com

コマンドラッパーです。実行したいコマンドを引数に指定して、実行すると色々やってくれるCLIツール。

ベースはhttps://github.com/Songmu/horensoのコードで、そこに自分が実務で使う機能を追加して、色々書き直させていただいた実装になっています。使い方も、想定される使用場面もhorensoと同様です。主にcronタスクの実行通知とかに使います。

github.com

songmu.jp

機能

  • コマンドの実行時、終了時、失敗時、成功時に任意のスクリプトを実行できるフックハンドラー。cronタスクの完了通知、失敗時のアラートなどを送ることができる。horensoreporternoticerと同様だが、もうちょい細かく実行ポイントを設定できる。
  • 標準出力とエラーをファイルに出力する、かんたんなロギング機能。これも本家のhorensoにある機能。
  • 多重実行の防止。
  • 指定時間を超過したときにタイムアウトさせる機能。

他にも、環境変数を設定したり、実行ユーザーやグループを変えたりする機能もあるのですが、作っておいて結局自分では使っていないので、あまり主張するようなものではないかも。

あと、特徴的な機能にLuaの実行環境が内蔵されています。これは、コマンド結果を通知するハンドラーの実装をサポートする目的で組み込んであります。

ハンドラーはhorensoの仕様と互換性があり、標準出力からコマンドの状態を表すJSONを受け取って任意の処理を行えます。コマンドの実行結果をSlackに通知する用途に使うならJSONを読んで、httpリクエストを送信するくらいで済むので、この内蔵Luaを使うことで、外部への依存なくハンドラーを実装できます。

なお実際にこのLuaで実装した、Slackに通知するハンドラーが以下にあります。

https://github.com/kohkimakimoto/crun/blob/master/handlers/crun-handler-slack

ちょっとトリッキーなshebangの指定をしているので、Luaのコードハイライトが効かず、読みづらいですが、ちゃんと動きます。

ちなみにLuaのランタイムは

github.com

を使用しています。

利用シーン

私自身としては

  • cronタスクに対して、チャットへの通知と多重実行防止、ファイルにログ出力
  • Webアプリのデプロイスクリプトに対して、チャットへの通知と多重実行防止

などに使っています。

Goでジョブキューを実装した

HQというGoで実装したジョブキューを公開しました。

github.com

WebのUIもあります。

HQ WebUI

概要

以下の特徴があります。

  • Goによる実装で、シングルバイナリ。
  • スタンドアロンのHTTP APIサーバー。ジョブのデータベースも組み込みであるため、別途特別な依存を必要としないで動作する。
  • シンプルでプログラミング言語非依存。HTTP APIでジョブを投入し、ジョブはHTTP POSTメッセージをワーカーアプリケーション(Webアプリ)に送信するというアーキテクチャ
  • フロントエンドとしてCLIとWebUIを組み込みでサポート。

上記のリポジトリのREADMEにも載せてありますが、ざっくりジョブのフローを図解すると、以下のようなアーキテクチャになっています。

HQアーキテクチャ

HTTP APIでジョブ(JSON)を投入します。HQはジョブを取り出し、ジョブに記載されたURLにHTTP POSTして、別途用意されたワーカー用のWebアプリケーションにジョブを実行させる、という流れになっています。

HQ自体は、HTTP APIベースのキューだけを提供しており、ジョブの優先度にもとづく制御や、細かなプロセスのスケジューリングなどはありません。単にFIFOでジョブをキューイングして、WebアプリのURLにHTTPリクエストを送信するだけです。

HQがキューイングしているジョブは組み込みのCLIかWebUIで管理できます。以下はCLIでジョブの一覧を表示させた例です。

HQコマンドラインインターフェース

背景

自分が開発しているPHPのWebアプリケーションに、非同期に処理を実行する機構が欲しかったので実装をはじめました。自分が必要とする要件から、実装方針を整理すると

  • 小規模のシステム構成に組み込めるように、シンプルなスタンドアロンのサーバープロセス。
  • プロセスダウン時にジョブをロストしない、最低限の信頼性のあるデータストア。
  • プログラミング言語非依存のインターフェース。
  • 対障害性や分散処理は現状では求めていない。また、これらはジョブキュー自体に備わっていなくても、別レイヤで対応できる。
  • ジョブの優先度設定や細かなスケジューリングができることは現状では求めていない。

となります。

技術解説

HTTP APIベースでのジョブキュー実装のアーキテクチャとして

blog.yuuk.io

github.com

をかなり参考にさせていただきました。基本的なコンセプトはほぼ一緒ですが、HQはよりシンプルに最小限のキューの実装と、DBを内蔵して外部依存を排除することで、小規模なWebシステムにも組み込みやすくしてあります。仮にシステムが大きくなった場合も、複数のインスタンスを立てて前段にロードバランサをおくなど、なんらかの対応ができると思っているので、まず小さく始めるのことができるように意識して実装しました。

キューからジョブを取り出すワーカースレッドを、Goルーチンで実装するコードは

github.com

の内部を参考にさせていただきました。

内蔵の組み込みデータベースは

github.com

です。トランザクションサポートありのKVSで、Goの組み込みデータベースとしてはかなり枯れているライブラリであると認識しています。

WebUIもバイナリにバンドルしてあります。TypeScript + React + WebpackによるSPAになっています。このへんは単に自分の好みと、新しめの技術に触ってみるためにあえて選んだ技術スタックになっています(今更ながらReact Hooksをはじめて使ってみました)。

実装後の所感

個人的に、Goはこのような独立性の高いミドルウェアを実装しやすい言語だと思っています。Httpサーバーをかんたんに実装でき、データベースのライブラリも豊富、WebUIむけのJavaScriptやHTMLなどのリソースをバイナリにバンドルするツールも揃っています。ところで、このようにスタンドアロンのサーバーを実装すると、機能面の実装のほかにも、実はいろいろと、考慮しないといけないことがあることに気が付きます。たとえば以下のようなことです。

  • シグナルへの適切な反応と、サーバーのグレイスフルシャットダウン。
  • ログのローテーション。または外部ツール(Linuxのlogrorateなど)でローテーションしたときにファイルハンドルを再オープンするための機構。

普段PHPなどでWebアプリを書くときは、アプリケーションサーバー(Apacheとかnginxとか)が面倒をみてくれるようなところです。そして、今回はWebUIもSPAで実装したので、これによって、サーバーサイドからフロントエンドまでの技術スタックを広く網羅して、小さくまとめた感じに実装できて、いい達成感を得ました。

自分の技術の棚卸しができた、という感じです。

MySQLのレプリケーション構築手順

久しぶりにそういうことをやったので、備忘録です。

前提と方針

  • マスター、スレーブ構成のレプリケーションを設定するための手順です。MySQLのバージョンは5.7を対象とします。
  • OSはCentOS7です。
  • レプリケーションに関わる基本的な概念(バイナリログ、サーバーID)は既知のものとして解説はしません。
  • レプリケーション設定以前のスタンドアロン環境での運用における設定は済んでいるものとします。
  • MySQL5.7では従来の方式に加えて、GTIDベースによるレプリケーションが利用可能になっています。マスター接続時にバイナリログのポジションを指定する必要がないなど、運用上のメリットが多いと判断したので、この方式で構築します。
  • レプリケーションの整合性、安全性を重要視するので行ベースのレプリケーションで設定します。

公式ドキュメントはここ

MySQL :: MySQL 5.7 Reference Manual :: 16 Replication

手順

マスターの設定

my.cnfを設定する。いろいろ他の設定も含めてありますが、以下のreplication (master)部分がレプリケーション関係の主な設定です。server-idをユニークにすることを忘れずに。個々の設定項目の詳細は公式ドキュメントを参照すること。

[mysqld]
server-id             = 1
datadir               = /var/lib/mysql
socket                = /var/lib/mysql/mysql.sock
symbolic-links        = 0
max_allowed_packet    = 100MB
transaction-isolation = READ-COMMITTED
slow_query_log        = 1
slow_query_log_file   = 'mysqld-slow.log'
long_query_time       = 1
max_connections       = 1500
max_connect_errors    = 1000000
log-error             = /var/log/mysqld.log
pid-file              = /var/run/mysqld/mysqld.pid
sql_mode              = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

# replication (master)
log-bin                  = mysql-bin
log-bin-index            = mysql-bin
binlog-format            = 'ROW'
expire_logs_days         = 30
enforce_gtid_consistency = 1
gtid_mode                = ON

# innodb
innodb_file_per_table
innodb_buffer_pool_size = 128MB
innodb_log_file_size    = 48MB

# fulltext search
innodb_ft_min_token_size = 1
ngram_token_size = 1
innodb_ft_enable_stopword = OFF

# character set
character-set-server = utf8mb4
collation-server = utf8mb4_bin
skip-character-set-client-handshake

[mysql]
default-character-set = utf8mb4

[mysqldump]
default-character-set = utf8mb4

レプリケーション用ユーザの設定を行います。接続元IPの指定やパスワードの設定を適宜、読み替えることを忘れずに。

$ mysql -uroot -p
> GRANT REPLICATION SLAVE ON *.* TO repl@'192.168.0.11/255.255.255.0' IDENTIFIED BY 'yourpassword';
> SELECT Host, User FROM mysql.user;

mysqldを再起動する。

# systemctl restart mysqld

スレーブの設定

my.cnfを設定する。レプリケーションの関連の部分のみ抜粋。他はマスターの設定と同様に。server-idをユニークにすることを忘れずに。

[mysqld]
# ...

# replication (slave)
log-bin                   = mysql-bin
log-bin-index             = mysql-bin
binlog-format             = 'ROW'
expire_logs_days          = 30
enforce_gtid_consistency  = 1
gtid_mode                 = ON
relay-log                 = relay-bin
relay-log-index           = relay-bin
relay_log_info_repository = TABLE
relay_log_recovery        = ON
read_only                 = 1
log_slave_updates         = 1

mysqldを再起動する。

# systemctl restart mysqld

マスターサーバのデータをスレーブにコピーする

ここではコールドバックアップによるマスターのスナップショット(データディレクトリを丸ごとtarで固めたもの)を使っておこないます。 まずマスターのmysqldを停止する。

# systemctl stop mysqld

データディレクトリをコピーしてtarに固める。

# cd /var/lib/mysql
# tar cpf /var/tmp/mysql-snapshot.tar .

スナップショットをスレーブにコピーする。スレーブのIPやSSHユーザー名は適宜読み替えること。

$ scp /var/tmp/mysql-snapshot.tar kohkimakimoto@192.168.0.11:/var/tmp/

スレーブサーバーにログインして、スレーブのmysqldを停止する。

# systemctl stop mysqld

スレーブのmysqlデータディレクトリにスナップショットを展開する。

# cd /var/lib/mysql/
# rm -rf ./*
# tar xpf /var/tmp/mysql-snapshot.tar
# ls -la

バイナリログを削除する。

# rm /var/lib/mysql/mysql-bin.*
# rm /var/lib/mysql/ib_logfile0
# rm /var/lib/mysql/ib_logfile1

サーバーごとのUUIDが設定されているauto.cnfを削除する。(次回起動時に再生成されます)

# rm /var/lib/mysql/auto.cnf

マスターを起動する

# systemctl start mysqld

スレーブを起動する

# systemctl start mysqld

スレーブのレプリケーションを開始する

準備が整いました。スレーブのmysqldに接続して、以下のコマンドを実行して、レプリケーションを開始します。

$ mysql -u root -p
> RESET MASTER;
> CHANGE MASTER TO MASTER_HOST='192.168.0.10', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='yourpassword', MASTER_AUTO_POSITION=1;
> START SLAVE;

スレーブの状態を確認します。

> SHOW SLAVE STATUS\G
Slave_IO_Running: Yes
Slave_SQL_Running: Yes

となっていることを確認できれば完了です。お疲れさまでした。

CentOS7でmunin + nginxを設定する

久しぶりにmuninのインストールと設定などを行ったら、いろいろやり方が変わっていたのでやったことのメモを残します。nginxのインストールは済んでいるものとします。

インストール

yum install munin munin-cgi munin-nginx

設定ファイル

/etc/munin/munin.conf

単純なサーバー統計を監視するだけなら、デフォルトのままでOK。特に書き換える必要なし。

/etc/munin/conf.d/

この配下にインクルードされる設定ファイルを配置する。監視対象のサーバー定義など。

例: /etc/munin/conf.d/servers.conf

[your-node-1]
    address 192.168.0.2
    use_node_name yes
[your-node-2]
    address 192.168.0.3
    use_node_name yes

nginxの設定

munin用にバーチャルホストを設定する部分のみを抜粋。大体こんな感じ。

server {
    listen       80;
    server_name  your-server-name.xxx.com;
    root /var/www/html;
    index index.html;

    location / {
        allow 127.0.0.1;
        deny all;
    }

    location ^~ /munin-cgi/munin-cgi-graph/ {
        access_log off;
        fastcgi_split_path_info ^(/munin-cgi/munin-cgi-graph)(.*);
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_pass unix:/var/run/munin/munin-cgi-graph.sock;
        include fastcgi_params;
    }

    location /munin/static/ {
        alias /etc/munin/static/;
    }

    location /munin/ {
        alias /var/www/html/munin/;
    }
}

この設定はhttp://your-server-name.xxx.com/muninでアクセスするための設定。サブディレクトリの配置をなくしたい場合は、rootのパスなどを適宜いじる。

サービスの起動

グラフはFastCGIで描画されるのでFastCGIサーバーを起動する。

systemctl start munin-cgi-graph.service

以上。

munin-node

監視対象サーバーに設定するmunin-nodeは割愛。yumでインストールしてsystemdで起動、ファイアウォールにport4949を許可するなどする。

公式ドキュメント

わからないことはドキュメントを読む。

https://munin.readthedocs.io/en/latest/index.html

メモ

  • (グラフだけでなく)HTMLも動的生成するためのFastCGIがある。が、うまく設定できなかったので保留。ドキュメント
  • FastCGIを使わずにmunin組み込みのhtttpdでグラフとHTMLを描画する機能もある。が、うまく設定できなかったので保留。ドキュメント

ブロックチェーンは暗号通貨に特化した技術で、応用はほとんどできないのでは?

最近、ブロックチェーンについていろいろ調べたので、自分なりの理解をまとめておきます。

この2つの記事が、現時点での自分のブロックチェーンに対する理解にとても近いです。 なので、詳細は元記事を参照するとして、以下はそれらを踏まえた自分なりの要約、理解の整理とします。

ビットコインブロックチェーン

結論。ブロックチェーンは現状、ビットコインのような暗号通貨のみに特化した技術で、応用は難しい。

その一方、ビットコインのシステムは、全体として本当にうまい具合に作られていて、よくこんなことを考えついたものだな、という驚きが調べていてありました。 純粋な情報技術だけでなく、人のインセンティブまで考えて、うまくシステムが設計されているからです。 逆に言うと、ビットコインで使われているブロックチェーンは、それだけ特定領域—暗号通貨、第三者機関が不要な送金プラットフォーム—に特化した設計になっていると感じました。

このシステムは、

  • ハッシュでつないだ連結リスト構造のデータベース
  • 中央機関のないパブリックなP2Pネットワーク
  • マイニングと手数料というインセンティブ設計
  • プルーフ・オブ・ワークによるブロック生成時のCPUコスト

などの要素からなり、これらすべてが相互にうまく機能しているからこそ、ブロックチェーンのメリットによく挙げられる

  • 記録に透明性があり、改竄が困難
  • 特定の管理者が不要
  • 二者間での低コストな取引

が実現できています。上記の技術要素の内いずれかでも欠いた場合、これらのメリットは失われます。 たとえば、パブリックなP2Pネットワークを使わない場合、ビットコインが実現しているレベルでの「記録に透明性があり、改竄が困難」は確保できませんし、インセンティブ設計が適切でなければ、そもそもパブリックなP2Pネットワークが維持できません。

このような前提条件をクリアできる領域は、現状、暗号通貨のシステムくらいしかなく、そうではない他の領域でのブロックチェーンは、ビットコインで使われているそれとは似て非なるもので、うたわれるメリットは限定的か、実現不可能なのではないか、という疑問を抱かずにはいられません。

ブロックチェーン技術の利用

今日のシステムにおいては、特定の企業または人物によって管理された中央サーバーによる運用が大多数です。これらのサーバーで動作するブロックチェーン(プライベートブロックチェーン)もありますが、これは環境がパブリックなP2Pネットワークでない以上、前述のように、ビットコインブロックチェーンとは、ほとんど別のモノです。従来の技術—RDBMSAPIを実装したり、デジタル署名による改竄防止—でも同じものが作れます。

このようなプライベートブロックチェーンは、従来のシステムより、構築、運用のコスト削減になるという優位性がある、という意見もあるようです。ただ、個人的にはその点も懐疑的です。

技術要素を冷静に考慮しよう

ビットコインブロックチェーンはひどく限られた条件でのみ有効に作用します。その前提を考慮せず、実体として異なるものをもってきているのに、ブロックチェーンという言葉だけで、それを「ビットコインのベースになっている革新的な技術」として認識すべきでありません。

Macで新しくターミナルウィンドウを立ち上げて、コマンドを実行する

アプリケーションの開発中、プロジェクトのディレクトリでターミナルを立ち上げて、テストやビルドプロセスを、コマンドラインから実行することが、よくあります。そのなかにはwabpack --watchなど、実行後プロセスが常駐して、開発をサポートするタイプのコマンドがあります。テスト用のDBや、開発用Webサーバの実行などもこの種類です。

このような常駐型のツールをターミナルから立ち上げると、ターミナルがブロックされます。この状態で他のコマンドの入力を行いたい場合、別にもう一つターミナルウィンドウ(またはタブ)を立ち上げる必要があります。いちいち手作業で新しいウィンドウを起動するのが煩わしいので、自動で常駐型プロセスの方を新しいウィンドウで実行できるように、起動スクリプトを書きました。

#!/usr/bin/env bash
set -e

# Open new terminal.
SOURCE="${BASH_SOURCE[0]}"
DIR=$(pwd)
if [[ "$(uname)" = "Darwin" ]] && [[ -z "$ON_NEW_TERMINAL" ]] && [[ -z "$NO_NEW_TERMINAL" ]] ; then
    osascript -e "tell app \"Terminal\" to do script \"cd $DIR && export ON_NEW_TERMINAL=1 && $SOURCE\""
    exit 0
fi

# Get the directory path and move it.
while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done
DIR="$( cd -P "$( dirname "$SOURCE" )/" && pwd )"
cd "$DIR"

# Run your command...
echo 'hello world!'

短いので見ればわかると思いますが、要はAppleScriptでTerminalアプリを起動しているだけです。Run your command...のコメント部分以下に、新しいウィンドウで実行したいコマンドを書いて、ラッパーとして使います。たとえば、以下はPHPを実行するapacheをDockerを使って立ち上げたい場合のコードです。

echo "Starting httpd server by using docker."
echo "Please open http://localhost:8080"
(sleep 1; open http://localhost:8080) &

docker run -i -t --rm -v $DIR:/var/www/html -p 8080:80 php:7.1-apache bash -c 'apache2-foreground'

これを記述して、このスクリプトを実行すると

f:id:kohkimakimoto:20180220194021g:plain

のように、新しいターミナルウィンドウが開いて、そこでapacheが起動します(この例ではついでにopenコマンドでブラウザも開いています)。

新しいウィンドウを開きたくない場合はNO_NEW_TERMINAL=1のように環境変数を設定して、スクリプトを実行すればよいです。

シェルスクリプトの代わりにPythonを使う

これまで、開発や運用時に使う、ちょっとしたコマンドラインツール、自動化スクリプトは、主にBashシェルスクリプトで実装していたのですが、最近このような用途にはPythonを使うようにしています。

Bashスクリプトへの不満

Bashスクリプト実装において、以下のような不満がありました。

  • クラスや連想配列がないので、構造化したデータが扱いづらい。
  • JSONをパースできない。jqなどのシステムにデフォルトでインストールされていないコマンドが必要。
  • 基本、コマンドの組み合わせでロジックを書いていくのだが、MacLinuxで挙動が微妙に異なるコマンドがある。そのため思わぬ環境依存でハマることがある。
  • 関数の戻り値が数値しか返せないので、結果を文字列で欲しいときは、標準出力とパイプを使うなど、いろいろ細かいテクニックが必要。

追記: 連想配列bashにもあるとコメントがあったので修正しました。調べたらバージョン4あたりからサポートされたようです。

ちなみにBashスクリプトを書く上でのテクニックは、rbenvとherokuのbuildpackの実装が参考になります。

また、以下は昔まとめたBashチートシートです。

ともあれ、ある程度複雑な実装になってくると、Bashだとプログラミング言語としての機能が貧弱なので、つらいものがあります。

スクリプトの実装方針

私がスクリプトを書く際の、実装の基本方針は以下のようなものです。

  • MacでもLinux(主にCentOS)でも動作する。
  • なるべく、システムに標準でインストールされているコマンド以外に依存しない。
  • なるべく、1ファイルで実装する。
  • -hオプションでヘルプメッセージ表示に対応させて、必要なドキュメントも内包させる。

要は「Mac上で実装、デバックして、Linuxサーバ上で使う場合は、スクリプトファイル一個を置けば動作する」ように作りたいわけです。PythonMacにもCentOSにもデフォルトで入っているし、標準ライブラリも豊富なので、このようなポータビリティ重視のスクリプト記述に向いていると思いました。

ポータビリティ重視のための縛りPythonプログラミング

Pythonスクリプトを実装していく中で、前述のポータビリティを確保するため、いくつか留意すべき制限があります。

  • Pythonのバージョン2系と3系どちらでも、動作するように書く。
  • 外部パッケージを使わない。

現在のPythonの主流はバージョン3系なので、基本は3系のコードを書きます。しかし、MacCentOSにデフォルトインストールされているPythonは2系なので、そちらでも動くように、適宜ワークアラウンドを入れて、書きます。とはいえ、大がかりなアプリケーションを書くわけではないので、そこまで難しいものではありません。外部パッケージを使わないという縛りは、基本的に実装を1ファイルで済ませたいからです。あくまで、シェルスクリプトの代替として使いたい、というのが今回の目的です。

Tips

以上を背景に、実際にPythonスクリプトを書く際に使っているTipsを紹介します。

__future__モジュールでバージョン2、3両方に対応させる

__future__モジュールを以下のようにしてインポートします。個々の仕様の詳細は、もう忘れてしまいましたが、3系のコードを、2系のランタイムでも使用できるようにするものです。今は何も考えず、おまじないのように、スクリプトの最初に必ず記述する感じです。

from __future__ import division, print_function, absolute_import, unicode_literals

バージョン2、3で異なるモジュールを、同じ名前で読み込む

ConfigParserは、iniファイル形式の設定ファイルを扱うための標準モジュールです。 Bashでは難しい、構造化された外部設定ファイルを扱えるので、便利です。ただ2系と3系でモジュールの名前が変わっているので、以下のようにして、最初に3系のモジュールをimport、失敗したら2系をimportするようにしてます。これで両方のバージョンで同じようにモジュールが使用できます。

try:
    import configparser as ConfigParser
except ImportError:
    # fallback for python2
    import ConfigParser

config = ConfigParser.RawConfigParser()
config.read("/path/to/configfile.conf")

バージョン2、3を判別する関数を用意する

どうしても2系と3系で、処理を切り分ける必要がでてくる場合もあります。このため、バージョンを判別できる関数を定義しておくと便利です。以下のようにruntimeクラスのスタティックメソッドとして実装します。

class runtime:
    @staticmethod
    def v3():
        return sys.version_info >= (3,)

    @staticmethod
    def v2():
        return sys.version_info < (3,)

if runtime.v3(): 
    # バージョン3系のときの処理を書く...

よく使うスニペット

コピペして使っているスニペットをいくつか紹介します。全て、標準モジュールのみに依存するコードです。適宜import osimport subprocessなどのように、事前に必要なモジュールをロードして使います。

文字に色をつける

class colors:
    bold = '\033[1m'
    underlined = '\033[4m'

    black = '\033[30m'
    red = '\033[31m'
    green = '\033[32m'
    yellow = '\033[33m'
    blue = '\033[34m'
    magenta = '\033[35m'
    cyan = '\033[36m'
    lightgray = '\033[37m'
    darkgray = '\033[90m'
    lightred = '\033[91m'
    lightgreen = '\033[92m'
    lightyellow = '\033[93m'
    lightblue = '\033[94m'
    lightmagenta = '\033[95m'
    lightcyan = '\033[96m'
    
    background_black = '\033[40m'
    background_red = '\033[41m'
    background_green = '\033[42m'
    background_yellow = '\033[43m'
    background_blue = '\033[44m'
    background_magenta = '\033[45m'
    background_cyan = '\033[46m'

    reset = '\033[0m'

# 使い方
print(colors.red + "red text" + colors.reset)

赤い文字でエラーメッセージを出力して終了する

前述のcolorsクラスを使います。

def abort(s):
    print(colors.red + s + colors.reset, file=sys.stderr)
    sys.exit(1)

abort("error!")

外部コマンド実行する

シェルスクリプトの代替としてのPythonなので、外部コマンド実行は、ほぼ必ず使います。subprocess.check_outputを使うと、簡単にコマンド実行して標準出力を取得できます。 このメソッドの戻り値は2系と3系で異なるので、前述のrumtimeクラスによるバージョン判定を使って、以下のように利用します。

out = subprocess.check_output("ls -la",  shell=True).strip()
if runtime.v3(): out = out.decode('utf-8')

テキストをファイルに出力する

fd = open("/path/to/file", 'w')
fd.write("""#!/usr/bin/env bash
set -e

echo "generated by Python"

""")
fd.close()

ファイルに実行権限をつける

umask = os.umask(0)
os.chmod("/path/to/file", 0o755)
os.umask(umask)

スクリプトの同時、多重起動防止する

try:
    fd = open(__file__, 'r')
    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
    print("Another process is using: " + os.path.basename(__file__), file=sys.stderr)
    sys.exit(1)

コマンドライン・オプションの解析

argparseモジュールでできます。以下は最小のサンプルです。

import argparse

parser = argparse.ArgumentParser(
    description="cli application description",
    formatter_class=argparse.RawDescriptionHelpFormatter,
    epilog="""
additional description...

"""
    )

parser.add_argument("-V", "--version", dest="version", action="store_true", help="Print the version.")
args = parser.parse_args()
if args.version:
    print("v0.0.1")
    sys.exit(0)

これだけで-hオプションで、ヘルプメッセージを表示に対応したコマンドができます。epilogに使い方などを書けば、ちょっとしたドキュメント代わりになります。 ほかにも、gitのようなサブコマンドの作成にも対応していて、Bashに比べると、とても便利です。

HTTPリクエス

以下は、githubapiから、公開鍵を取得するサンプル。ついでにBashでは難しい、JSONのパースも。

#!/usr/bin/env python
from __future__ import division, print_function, absolute_import, unicode_literals

try:
    from urllib.request import urlopen, Request
    from urllib.error import HTTPError
except ImportError:
    # fallback for python2
    from urllib2 import urlopen, Request, HTTPError
import json
import sys

class runtime:
    @staticmethod
    def v3():
        return sys.version_info >= (3,)

    @staticmethod
    def v2():
        return sys.version_info < (3,)

def main():
    res = urlopen("https://api.github.com/users/kohkimakimoto/keys")
    body = res.read()
    if runtime.v3(): body = body.decode('utf-8')

    keys = json.loads(body)
    for key in keys:
        print(key['key'])

if __name__ == '__main__': main()

まとめ

Bashより格段に便利です。pipなどで外部パッケージを使わなくても、ちょっとした自動化スクリプトなら充分カバーできます。