くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログ。積極採用中です。

TypeScriptを使ってDiscord botを作ってみよう!

はじめに

こんにちは!今年新卒入社しました、エンジニアのタナカです。

早速ですが、ここ最近リモートワークが推奨されている中、「Discord(ディスコード)」というツールを導入した、もしくは導入してはいないけれども単語は聞いたことがあるという方が増えているのではないでしょうか。

このツールは過去に 「ゲーマー向けチャットツール」 とよく言われてましたが、2020年7月にブランディングイメージが 「あらゆるコミュニティが使えるコミュニケーションツール」 に変更され、ゲームだけに留まらないツールとなりそうです。

音声も結構クリーンな感じなのもまたいいところですよね👍

さて、この「Discord」というツールなのですが…実はこれ、チャットや会話をするだけではなく、botだって作れるんです。

私も実際にbotを作ってみましたが、とても楽しく作れます!

「プログラミング一通り学んだけど次何したら良いか分かんない…」という方にもきっとオススメできます。

なので今回は、TypeScriptでのDiscord botの作り方(コード部分)について書いていきたいと思います!

※本記事は9/18(金)に協賛枠で参加したWebナイト宮崎 Vol.10 ~てげTypeScriptを学びたい~にて Tunakan という名義で発表した内容から一部を抜粋し、ブログ用に編集したものになります。

 

Discord bot 作ってみよう!

準備

事前に必要なものは以下になります。

…え?これだけ?

そうです。これだけで作れちゃいます!

24時間フル稼働させるような規模のものでなければ、たったの3つで出来ます!

ちなみに今回JavaScriptではなくTypeScriptを使う理由としては、くらしのマーケットで使われている言語の1つなので使い慣れているという点があります。

そして以下が作成したサーバーにbotを追加した状態になります。

f:id:curama-tech:20200924134126p:plain

右側に 「Tunakan bot というのがオフラインで存在してますね。こちらが今回準備したbotになります。

…おや?何やらDiscord画面の下の方に面白い言葉が書いてありますね?

f:id:curama-tech:20200924134221p:plain

「ご挨拶しな!」と半ば強引に挨拶を求められてますね…😅

ただこの状態だと(作成してすぐなので当然ですが)何も返してくれません。シカトされちゃいます。悲しいですね…

なので、まずは簡単に挨拶出来る機能を追加しちゃいましょう!💪

botが挨拶出来るようにしよう!

最初に、discord.jsというNode.jsモジュールをインストールします。MacLinux系ならターミナル、Windowsならコマンドプロンプト、その他お好きなコマンドラインツールがあればそちらを開き以下を実行しましょう!

npm install discord.js

次にテキストエディタを開いて以下のコードをサクッと書いて保存してください。

// ./bot.ts

import { Client, Message } from "discord.js";
import { config } from "./config";

const client = new Client();

interface ReplyInterface {
    messageReply(message: Message): Promise<void>;
}

class Reply implements ReplyInterface {
    public async messageReply(message: Message): Promise<void> {
        if (message.author.bot) {
            return;
        } else if (message.content === "こんにちは") {
            message.reply("こんにちは!");
        }
    }
}

const reply = new Reply();

client.on("message", (message) => reply.messageReply(message));

client.login(config.token);
// ./config.ts

export const config = {
    token: "{token}",   // Developer Portalからtokenをコピペする
};

ファイル構成は以下のようになるはずです。

f:id:curama-tech:20200924134244p:plain

このサクッと書いたコードですが、以下の流れになります

  1. client.loginbotがオンライン状態になる
  2. 何かしらのメッセージが送信されたらreply.messageReply(message)を実行
  3. if (message.author.bot) { return; }で、メッセージがbotからならreturn
  4. bot以外で"こんにちは"というメッセージが送信されたら"こんにちは!"と発言者に対してリプライをする

bot作るなら結構複雑なコード書かなきゃいけないんじゃ…?と思われそうですが(実際私もそう思っていました)、単純なものならこの通りたった数行で作れます!👍

では実際に動かしてみましょう!以下をコマンドラインツールで実行してみてください!

.tsファイルのコンパイルを行った後、botを起動します!

tsc bot.ts
node bot.js

f:id:curama-tech:20200924134306p:plain

f:id:curama-tech:20200924134320p:plain

うまく行けばこの通り、Tunakan botがオンライン状態になり、挨拶をしてくれます!

挨拶だけじゃ物足りない!もっと凝ったものを作る

さて、挨拶するだけじゃかなり物足りないですよね?

せっかく簡単に作れるんだから、何かもうちょっと凝ったものを作りたい…少し手軽にbotらしく動作させたい…うーん…

そうだ、じゃんけんゲームを作ろう!

というわけでじゃんけんのコードをサクッと書いちゃいましょう。

import { Collection } from "@discordjs/collection";
import { Client, Message } from "discord.js";
import { config } from "./config";

const client = new Client();

enum hands {
    "グー",
    "チョキ",
    "パー",
}

interface ReplyInterface {
    messageReply(message: Message): Promise<void>;
}

interface GameInterface {
    playRockPaperScissorsGame(message: Message): Promise<void>;
}

class Reply implements ReplyInterface {
    private game = new Game();

    public async messageReply(message: Message): Promise<void> {
        if (message.content === "じゃんけん") {
            await this.game.playRockPaperScissorsGame(message);
        }
    }
}

class Game implements GameInterface {
    public async playRockPaperScissorsGame(message: Message): Promise<void> {
        await message.reply("最初はグー!じゃんけん…!");
        const filter = (player) => {
            return ["グー", "チョキ", "パー"].includes(player.content);
        };

        let player: Collection<string, Message>;
        try {
            player = await message.channel.awaitMessages(filter, { max: 1, time: 10000, errors: ["time"] });
        } catch {
            message.reply("タイムオーバー!あなたの負けです");
            return;
        }

        if (!player) {
            // playerがundefinedならログアウトしてエラーを出す
            client.destroy();
            throw new Error("player is undefined");
        }

        // botの手を決める
        const botHand = Math.floor(Math.random() * 3);
        await message.reply(hands[botHand]);

        // 判定を行う
        const judge: number = (hands[player.first().content] - botHand + 3) % 3;
        switch (judge) {
            case 0:
                await message.reply("あいこ");
                break;
            case 1:
                await message.reply("あなたの負け");
                break;
            case 2:
                await message.reply("あなたの勝ち");
                break;
            default:
                await message.reply("Error!");
                break;
        }
    }
}

const reply = new Reply();
client.on("message", (message) => reply.messageReply(message));
client.login(config.token);

処理の流れは以下の通りです。サクッと作成したものなので、あいこでも再度勝負をしない一発勝負という点にご注意ください。

  1. ユーザーが「じゃんけん」とメッセージを送るとplayRockPaperScissorsGame()を実行
  2. 「最初はグー!じゃんけん…!」とreplyし、ユーザーからのメッセージをawaitMessagesで待つ
  3. filterで設定した文字列「グー」「チョキ」「パー」のメッセージが送信された時、bot側の手をランダムで決め、botの手をreplyする
  4. 計算してじゃんけんの判定を行い、それぞれの結果をreplyする

また、じゃんけんの判定ロジックは以下になります

  1. グー(0) チョキ(1) パー(2)とする
  2. ( {プレイヤーの手} - {botの手} + 3 ) % 3 をする
  3. 計算結果が0ならあいこ、1なら負け、2なら勝ちになる

コードが書けたらbotを起動してみましょう!tsc bot.tsコンパイルも忘れずに!

f:id:curama-tech:20200924134337p:plain

さいごに

Discordのbotは気軽に作れて、プログラミングの勉強にもなると思います!

Discordを使っている、サーバー持ってたりしててもうちょっといい感じにしたい!楽に運用したい!って時や、TypeScriptを勉強したい…!って時に作ってみるのもいいかもしれませんね👍

ログイン画面の「当たり前」を調査

みんなのマーケットでUI/UXデザイナーをしているミソサクです。

モバイルデザインの「当たり前」の変化はすごい早いです。その早さに置いていかれないように、今回は2020年9月現在の「ログイン画面」における「当たり前」を調査します。

まず、我らがくらしのマーケットのログインページはこちら。

くらしのマーケット

よくある感じですね、さて、置いていかれていないか調査に参ります。

世の中のログイン画面をみてみよう

Googleのログイン画面

Google

「メールアドレスまたは電話番号」と「パスワードを入力」する画面が別のページです。セキュリティ面の理由もありそうですが、ユーザーがログイン情報を覚えていないケースでは、この方が間違いがどちらにあるのか分かりやすそうですね。アプリでログインしようとしてもブラウザ版が起動するのも特徴です。「を入力」がパスワードの方にしか書いていないのは文字数が原因でしょうか。

Amazonのログイン画面

Amazon

「Eメールまたは携帯番号アカウントの番号」と「Amazonのパスワード」。 パスワードにわざわざ「Amazonの」と付いているのはなぜでしょう。 「アカウントの番号」なんてあるんですね、初めて知りました。

Facebookのログイン画面

Facebook

「電話番号またはメールアドレス」と「パスワード」の組み合わせです。シンプルで分かりやすく、今一番見かけるパターンな気がします。

Instagramのログイン画面

instagram

「電話番号、ユーザーネーム、メールアドレス」と「パスワード」の組み合わせです。サービスの特徴としてユーザーネームで覚えているユーザーが多そうですね。

LINEのログイン画面

LNE

「メールアドレス」と「パスワード」の組み合わせです。とってもシンプル!

Twitterのログイン画面

Twitter

「電話番号、メールアドレスまたはユーザー名」と「パスワード」の組み合わせです。Twitterもサービスの特徴としてユーザー名で覚えているユーザーが多そうですね。

Yahoo! JAPANのログイン画面

Yahoo! JAPAN

「ID/携帯電話番号/メールアドレス」と「確認コード」の組み合わせです。こちらも「ID」に当たる部分と「パスワード」に当たる部分の画面が別です。特徴としては、メールアドレスの場合でも、PCからのログインの場合でも、確認コードでログインする所です。パスワードを覚えなくていいのは便利ですね!

メルカリのログイン画面

メルカリ

まず最初に「アカウント連携」か、「メール・電話番号でログイン」かを選択します。 アカウント連携を選択するのユーザーが多いのでしょうか。「メール・電話番号でログイン」を選択後、「メールアドレスまたは電話番号」と「7文字以上の半角英数字」の組み合わせです。

UberEATSのログイン画面

UberEATS

「携帯電話番号」入力後、「OTPコードを入力してください」に4桁のコードを入力。SMSがうまくいかなかった時のために「パスワードを使用してログインする」が用意されています。こちらもブラウザ版が起動します。

TikTokのログイン画面

TikTok1

まず最初に「電話番号/メール/ユーザー名を使う」か、「アカウント連携」かを選択します。選べるアカウント連携の選択肢の数がすごいです!7つもあります! 「電話番号/メール/ユーザー名を使う」を選ぶと「ログイン」ページへ遷移し、タブで「電話」か「メールアドレス/ユーザー名」を選べます。

TikTok2

電話番号を入力すると4桁のコード入力画面に遷移しますが、こちらにも下の方に「パスワードでログイン」の導線がありました。クリックすると「パスワードを入力してください」の画面に行きます。SMSがうまくいかないケースが結構あるのでしょうか。

TikTok3

「メールアドレス/ユーザー名」のタブの中は「メールアドレスもしくはユーザー名」と「パスワード」の組み合わせでした。

TikTokは、ログインひとつに用意している選択肢の数が、圧倒的に多いですね!絶対ログインさせてあげるぞ!という気迫を感じます。

最後に

今回の調査で、「ログイン画面の当たり前」に若干遅れをとっていると感じました。「電話番号」が当たり前になりつつありますね。

「IDとパスワードは別のページ」という構成は、私の中でもっと多くのサービスの「当たり前」になっている印象でしたが、まだ「当たり前」にはなっていないのかなと感じた調査でした。

1,2年後に同じページの何がどう変化するのか、続編の調査も行いたいと思います!

AWS Summit Onlineにて、くらしのマーケットのインフラを紹介しています

昨日9/8より開催の、AWS Summit OnlineのAWS Startup Talksにて、「くらしのマーケット」のインフラ構成や、放映しているTVCMの負荷対策について、CTOの戸澤がご紹介しています。

aws.amazon.com (みんなのマーケット で、ページ内検索をお願いします)

くらしのマーケットは、2019年11月よりTVCMの放映をしています。 それまでスパイクのリクエストを処理した経験があまりなく、かつ、TVCMでどれほどの負荷が発生するかわからない状態でした。 予算を投じてTVCMを放映しても、ダウンしては意味がなく、更にユーザーにとってはネガティブな体験になります。 まずは負荷試験などできることから着手し、時間に限りもあったため、AWS含め対策を行い、放映を乗り切ることができました。 それらについて、また、業務で使用しているAWSサービスについても紹介しています。

ぜひご覧ください!

オンライン決済サービスをリリースしました

こんにちは、バックエンドエンジニアの @akira です。
今回は8月12日にリリース致しました、オンライン決済サービスの開発について振り返ってみようと思います。

今まではくらしのマーケットの支払い方法として現金のみ利用可能でしたが、オンライン決済サービスがリリースされたことでクレジットカードも選択できるようになりました。オンライン決済利用可能店舗は順次拡大しています。

くらしのマーケットにオンライン決済を導入することの難しさ

くらしのマーケットでは、以下 4 つのフェーズが主に存在します。

  1. ユーザーが予約を入れる
  2. 出店者が作業日を確定する
  3. 作業日当日、出店者がサービスを提供する
  4. 出店者がお会計をする

今回のリリースにより、ユーザー側の予約キャンセルも可能になりました。ユーザーのキャンセルはキャンセル料金が発生するケースもあり、そのキャンセル料金を出店者が回収できることが今回リリースしたオンライン決済のメリットです。

作業日当日にオプション作業を追加したり、事前に見積もっていた作業が不要になるケースもあり、予約時の金額とお会計の金額が異なるケースがあります。もし出店者がお会計の金額を誤った場合、金額の訂正を社内システムから行うこともできます。

これらの整合性を取りつつ、カード決済を導入することは非常に複雑で大変でした。

Technology Stack

今回リリースしたオンライン決済サービスの技術スタックは、以下の通りです。

  • TypeScript
  • NestJS
  • TypeORM
  • PostgreSQL
  • Domain-Driven Design
  • Clean Architecture
  • Docker
  • AWS Fargate
  • Stripe

言語は

  • 社内で長らく使ってきた言語
  • 型付き言語

の二点から TypeScript を選択しました。次点としては Golang などもありました。

また、今回初めて Domain-Driven Design(ドメイン駆動設計、DDD)を採用しました。DDD については後ほど詳しく触れます。

決済代行会社は Stripe を利用しています。

アプリケーションは AWS Fargate 上にデプロイして稼働しています。

DDD を 0 から理解する

私は昨年9月にみんマに入社したのですが、社内システムのバグを少し修正した後で今回のプロジェクトに参画しました。ちょうどその頃、オンライン決済のユーザー側開発がスタートしたばかりであり、私が最初に取りかかったタスクは、既存機能の一部を DDD を用いてアプリケーションコードのみリプレイスすることでした。

結果として、このリプレイスは失敗しました。この失敗談は、6月10日に開催しました「失敗に学べ!くらしのマーケットの開発「失敗」LT 会【オンライン/入退出自由】vol.1」にて発表しました。

minma.connpass.com

スライドは Speaker Deck にありますので、ご覧ください(趣旨:DB スキーマをリプレイスせずにアプリケーションコードのみ DDD で書き直すのは、DDD の考え方に反するものであり、アンチパターンである)。

speakerdeck.com

リプレイスに失敗後、本格的に DDD を学び直すことを決意し、Eric Evans 著書の『Domain-Driven Design: Tackling Complexity in the Heart of Software』(原著)を読破しました。このインプットにより、DDD の理解が非常に明確になりました。

ドメインの理解を深める

DDD を理解し、開発スピードが上がってきた一方で、ドメインの理解不足が顕著になってきました。DDD ではビジネスドメインの理解が必須になります。このビジネスドメインにはさらに二つ意味があると個人的に思っていて、

の双方を理解する必要がありました。

「ビジネスドメインそのもの」については、チームの PdM に詳しい仕様などを確認することができましたが、「ビジネスドメインが扱う領域のドメイン」(オンライン決済・クレジットカードの仕組み)については、きちんと理解していませんでした。

そこで書籍『カード決済業務のすべて―ペイメントサービスの仕組みとルール』を購入し、チームで回し読みすることで理解を深めました。この理解が後ほどオーソリの各種操作を実装する際に役立ちました。

DDD をチームに浸透させる

本格的に DDD でチーム開発を進めるため、チームの PdM や QA メンバーと打ち合わせを行い、認識をすり合わせました。大まかに以下の内容を共有しました。

  • ビジネスドメインの知識を常にアップデートすること
  • ビジネス用語ではなく、ユビキタス言語を使って開発者と会話すること
  • 開発者がビジネスドメインについて間違った理解を示していた場合は、必ず指摘すること

上記は基本的に普段の業務とかけ離れたものではないため、そこまで違和感もなかったのではないかと思っています。ただし、二点目のユビキタス言語を使って会話する部分は、開発を進めていくと大変だったように思います。

DDD で開発し始めた頃は、ビジネスエキスパート( PdM や QA メンバー)と開発者でそれぞれ違う単語で同じ事象を説明することが多く、それによって仕様の理解が異なってしまうことが多々ありました。後半になると、自然と認識の擦り合わせを積み重ねていたためか、ほとんど発生しなくなりました。


ここまでの技術的な詳細については、3月末に公開しました記事「DDD 開発をしてみての振り返り」に書かれていますので、合わせてご覧ください。 tech.curama.jp


主要機能を実装

ドメインモデルの数多くのリファクタリングを経て、ビジネスロジックの実装フェーズに入りました。この時は「ビジネスドメインが扱う領域のドメイン」(オンライン決済・クレジットカードの仕組み)についてはある程度知識がありましたが、一方で実装対象であるビジネスドメインそのものについては理解不足でした。

フローチャートを活用して、チームメンバー全員で処理フローを徹底的に理解したことと、仕様を理解しているかの確認を行う目的で、あまり詳しくないと実感している機能の仕様を説明し合う場を設けることで、ビジネスドメインそのものについての理解を深めました。

また、Stripe API についての理解も不足しており、ひたすらドキュメントを読んで API を実行し続けました。

Stripe API の制約を、 Clean Architecture のインフラ層とドメイン層のどちらに実装するかは難しかったですが、API を call することでチェックできるものはインフラ層、それ以外のものはドメイン層に実装しました。

ビジネスドメインの知識や Stripe の知識が揃ってきたタイミングで、主要機能の実装スピードも上がってきました。

DDD で気をつけるべきこと

無事オンライン決済サービスを DDD で実装することができたのですが、いくつか反省点がありましたのでお伝えしたいと思います。

ユビキタス言語が定まらないと、永遠にリファクタリングが続く

DDD は、ビジネスエキスパートと開発者の間で定義したユビキタス言語をそのままコードとして実装する設計手法です。そのため、開発を始める段階で、ある程度ユビキタス言語が定まっている必要があります。逆にいつまでもユビキタス言語に揺らぎがあると、それだけコードも常にリファクタリングし続けなければなりません。

よって、全てのユビキタス言語をきちんと定義した上で DDD の開発に着手するのが理想です。その状態でスタートできれば、ドメインモデルの再設計(リファクタリング)の頻度を減らすこともでき、DDD 開発のスピードアップが見込めるでしょう。

現実的には、 DDD を採用する前にユビキタス言語を整理し、将来的にどの程度変更される可能性があるかを事前に把握しておくと、ドメインモデルの設計段階で「このユビキタス言語が変更になる可能性があるので、このようにリファクタリングされるかも」などと想定することができます。

事前にビジネスドメインをよく理解しておく

DDD はユビキタス言語が変更されたタイミングで、ドメインモデルも即座にリファクタリングすることを推奨しており、初期の段階ではこれに従っていたのですが、後になればなるほどリファクタリングのコストが大きくなっていくのを痛感しました。

リファクタリングしなければいけないことに変わりはないため、できる限り初期のうちに完成形を目指してドメインモデルを設計すべきです。そのためには、ラフなドメインモデルを設計するタイミングでビジネスドメインについてある程度詳細な知識が必要でした。今回はそれが不足していたため、後半に入っても必要に応じて大胆にリファクタリングを実施してきました。

学習コスト

DDD に則った開発は、その概念(Strategic Design)から実装方法(Tactical Design)まで一定の形式に従う必要も出てきます。プログラミング言語であれば Object Oriented Programming が可能な言語、アプリケーションアーキテクチャーであれば Layered Architecture (あるいはその発展形)を採用することも推奨されています。

ある程度キャリアのあるエンジニアであれば、それらを学習してアウトプットすることは問題ないかもしれないですが、メンバーの中には比較的キャリアの浅いエンジニアもおり、その方々の学習コストをあまり考慮できていませんでした。

また、 DDD のコードをレビューできるメンバーが自チームのみ限られてしまう、といった事象も発生し、学習コストの高さを感じました。

DDD のメリット

一方、DDD を採用したことで大きなメリットもありました。

コードがとにかく綺麗

DDD と Clean Architecture が非常に相性が良く、コードが非常に分かりやすいものとなりました。

ドメインモデルをリファクタリングする際も、基本的にドメイン層だけ変更すれば良いですし、バグが見つかった場合の修正も比較的容易でした。

ビジネスドメインをきちんと理解した上で開発できる

これはむしろ DDD の恩恵そのものと言えますが、ユビキタス言語をそのままコードとして実装するため、ビジネスドメインを理解した上で開発することができます。

特に今回は、前述の「ビジネスドメインが扱う領域のドメイン」(オンライン決済・クレジットカードの仕組み)まで理解した上でコードをかけたのが一番良かったです。

DDD はクックブックではない

最後に、DDD の開発を一通り経験して個人的に主張しておきたいことがあります。それは「DDD はクックブックではない」ということです。

DDD の価値はその開発プロセス自体にあり、上部だけを擬えてもあまり効果は出ないと私は思います。

『Domain-Driven Design: Tackling Complexity in the Heart of Software』が出版されたのは 2003 年ですから、DDD を採用し始めた頃は「これってちょっとレガシーな設計手法なのでは」と正直思ってしまったこともありました。

しかし、DDD は概念の一種であるため、いつまでも応用ができます。特に Strategic Design の部分は、どれだけ技術が移り変わっていっても不変ではないでしょうか。

一方で、 Tactical Design の部分は時代とともにアップデートされていくでしょう。その一例として、昨今流行の Microservice Architecture があります。

よく「一マイクロサービスは、一つの境界づけられたコンテキストに相当する」といった考え方を見受けますが、Eric Evans 氏はこの考え方に警鐘を鳴らしています。

prescriptive guidance such as "each microservice is a bounded context" approaches the "cookbook" end of the spectrum, and diverges from the sweet spot of DDD.

「こうすればできる」といった方法論はクックブックに過ぎず、そのやり方では DDD の真価を発揮することはできないでしょう。そのため、本格的に DDD で開発をするのであれば、DDD を根本から理解することから始めるべきだと私は思います。

BashとPSQLを使う時の注意事項

こんにちは!エンジニアのカーキです。 今日はBashPSQLを使ってSQLを実行した時にハマったエピソードについて紹介したいと思います。

なにでハマったのか?

自分が書いたBashスクリプトpsqlを使っていくつかの簡単なSQL (主にINSERT, UPDATE, DELETE)を実行しようとした時にスクリプトが成功してもデータベースには期待していた変更がありませんでした。

実行していたSQLは今まで成功していたSQLだったので正しいと思い込んでスクリプトの動作だけで確認してたのですが、実はpermission 関係でSQLが失敗していたようです。ただ、Bashスクリプトのエラー時の設定とpsqlの実行方法によってSQL上のエラーに気付かず、スクリプトが最後まで走って、成功したように見えたいただけでした。

エラーに気付かなかった要因とその解決方法

要因 1.

Bashはデフォルトで実行途中でエラーになっても次に進むので注意しなければならない。

例: 以下のスクリプトを実行すると、cd のところでエラーになりますが、スクリプトはそこで中断されず、次の echo コマンドまで実行されてしまう。

#!/bin/bash

echo "This should run"
cd 存在しないパス
echo "This should not run!"

実行結果:

~/workspace ❯ ./hoge.sh
This should run
./hoge.sh: line 4: cd: 存在しないパス: No such file or directory
This should not run!

~/workspace ❯ 

解決方法:

-e フラグをセットすることで、エラーになったらすぐにスクリプトが止まるようにすることができます 。-e をセットするとBashが各コマンドの終了ステータス (Exit status)を確認し、0(成功)以外の終了ステータスが出たらスクリプトを止めてくれるみたいです。

-eフラグは以下のいずれかのやり方でできます:

  • シバンに -e を付ける
    #!/bin/bash -e

    echo "This should run"
    cd 存在しないパス
    echo "This should not run!"
    #!/bin/bash

    set -e
    echo "This should run"
    cd 存在しないパス
    echo "This should not run!"

実行結果:

~/workspace ❯ ./hoge.sh
This should run
./hoge.sh: line 4: cd: 存在しないパス: No such file or directory

~/workspace ❯ 

要因 2.

Postgresを使っていればpsql (Postgresのクライアント) を使ってSQLを実行することがよくありますが、その時に注意しないとエラーに気付かない場合があります。

psqlSQLコマンドを実行したい時に、ファイルから実行する方法と直接 psqlコマンドに-cオプションで実行する方法がありますが、それぞれ少し挙動が違います。 -cで実行した場合SQLでエラーになったらpsqlコマンドの終了コードが1 (エラー) になりますが、ファイルを流してSQL実行した時に終了ステータスが 0になります。

ということは、もしBash-cSQL実行してエラーになったらBash的にエラー扱いになりますが、同じSQLをファイルで流して実行すると終了ステータスが0になるので成功扱いになってしまいます。

例: 必ずエラーになるSQLを両パターンで実行してみましょう。 エラーになるように存在しないテーブルをSELECTしてみます。

  • -cオプションで実行した場合:
    ~/workspace ❯ psql -U postgres -h localhost -p 5432 postgres -c "SELECT * FROM foo"    
    ERROR:  relation "foo" does not exist
    LINE 1: SELECT * FROM foo
                        ^

    ~/workspace ❯ echo $?
    1    
    👆 終了ステータス !=0 なのでエラー扱いになる
  • ファイルに同じSQLを書いて実行した場合:
    ~/workspace ❯ psql -U postgres -h localhost -p 5432 postgres < foo.sql
    ERROR:  relation "foo" does not exist
    LINE 1: SELECT * FROM foo
                        ^

    ~/workspace ❯ echo $?
    0
    👆 終了ステータス = 0 なので成功扱いになる

例の通り、psqlでファイルを流してSQL実行した時にSQLでエラーが出ても終了コードが0になるので、エラーになった時に気付きづらいです。こうなるといくらBash-e フラグをセットしてもエラーとして扱われないので危険です。

解決方法:

psql コマンドに ON_ERROR_STOPオプションをセットすれば、エラーになったらすぐにスクリプトが中断されるようになります。

例:

~/workspace ❯ psql -v ON_ERROR_STOP=1 -U postgres -h localhost -p 5432 postgres < foo.sql
ERROR:  relation "foo" does not exist
LINE 1: SELECT * FROM foo
                      ^

~/workspace ❯ echo $?
3
👆 終了ステータス != 0 なのでエラー扱いになる

以上Bashpsqlを使った時の注意事項でした。終了ステータスをちゃんと意識しないとハマりやすいところだと思うのでみなさんも気をつけましょう!