メモ：表計算ベースアプリとしての Googleスプレッドシートのサクサク開発俺的ベストプラクティス2017 其の壱

# この記事は何
百姓が Googleスプレッドシート上で作っている、営農上必要な表計算ベースアプリ、それをサクサク開発するための現時点でのベストプラクティス。俺得メモ。

## 背景
出荷管理、顧客管理、確定申告向けの帳簿管理などを Googleスプレッドシートをベースに独自の機能を追加したものでやってそろそろ 4 年。しょっちゅう使ってはいてもスクリプトをいじるのはごくたまに。
Googleスプレッドシートからの脱却を何度も考えたけど、ここ最近 (半年くらいかな) はオフライン機能も強化され、スクリプトの体感的な実行速度も上がったように思い、もう少し使おうかな、という気にもなってきた。
いろいろ開発のサクサク感 (客観的時間短縮よりも主観的時間短縮を重視) を上げる工夫をしているけれど、いかんせんスクリプトをいじるのがたまーになので、意図を思い出すのに苦労する、ということで、Qiita にメモを残しとくよ。

###本題の前に拡張機能はメニューバーよりサイドバーから呼び出すとよいよ、という話
これは別の記事にまとめようと思うけど、ユーザが明示的に機能を呼び出す方法として、メニューバーに独自メニューを用意する方法と、サイドバーを表示してそこにボタンなどを配置する方法があるけれど、後者のほうが便利なことが多いと思う。
一点挙げると、メニューバーから呼び出すと実行結果が返ってこないので直接は表示できない (`SpreadsheetApp.toast()` 使って表示するとか、シートに書き込むとかすればいいけれど) けど、サイドバーなら `google.script.run` で成功・失敗時のハンドラを設定できるので、結果をサイドバーに表示することができる、とか。

### gas-manager を使ってローカルコーディング
Google Apps Script をローカルで開発するために[gas-manager](https://github.com/soundTricker/gas-manager)を利用させてもらっている。素晴らしいツールなんだけれど、いくつか使いづらい点がある (あくまでも個人の主観です)。個別のファイルじゃなくてプロジェクト内の全ファイルを対象に upload/download しなければならない点とか、新規プロジェクトや新規ファイルを作れない点とか、upload のつもりでうっかり download しちゃうとローカルの変更点が上書きされてしまうとか、同一プロジェクトのファイルは同じディレクトリに置くことがほとんどなのにファイルごとにプロジェクトファイル (`gas-project.json`) に全ファイルのフルパスを書かなければならないことなど。根本的に変えたい部分もあって gas-manager を基に別ツールの開発していたけれど、開発にかけられる時間があまり取れずなかなか形にならず。
gas-manager に簡単に機能追加できる部分については pullreq を送って取り込んでもらえたものもあり。

## three directories 方式
あるとき、ふと気づいた。gas-manager はコマンドラインオプションでプロジェクトファイルを指定できるんだった、と。
で、ローカルに「download の受け入れ場所 aport (arrival port)」「メイン開発場所 park」「upload に送り出すブツ置き場 dport (deperture port)」を用意することにしてみた。図にするとこんな感じ。
![three-directories.png](https://qiita-image-store.s3.amazonaws.com/0/64927/ee331f6a-32b1-5d05-179a-28bebcf1eeb2.png)
download したものは aport に入るので、それをいじっている park が上書きされることはない。リモートとローカルの差分は download したうえで aport と park を比べればよい。

説明がいろいろ面倒になってきたので `Makefile` を置いとこう。

```basemake:Makefile
GAS = ~/gas-manager/node_modules/.bin/gas
GASCONF = gas-config.json
DOWNCONF = project-download.json
UPCONF = project-upload.json
PROJCONF = project.info.json
UPLOAD_TARGETS   = $(addsuffix .u,$(notdir $(wildcard aport/*)))
DOWNLOAD_TARGETS = $(addsuffix .d,$(notdir $(wildcard aport/*)))
MK_PROJECT_JSON = mkprojectjson.js
APORT_DIR = $(abspath aport)
DPORT_DIR = $(abspath dport)

.SUFFIXES: .u .u- .d .d-

.PHONY: help
help:
	@echo -e "\tmake y9x.u : y9x プロジェクトのアップロード"

$(UPLOAD_TARGETS): $(UPCONF)
$(DOWNLOAD_TARGETS): $(DOWNCONF)

.u-.u: $(UPCONF) $(PROJCONF)
	$(GAS) -c $(GASCONF) -s $(UPCONF) upload -e $(basename $<) 2>&1 | tee $@ 1>&2
	touch --date='3 seconds' $<

.d-.d: $(DOWNCONF) $(PROJCONF)
	$(GAS) -c $(GASCONF) -s $(DOWNCONF) download -e $(basename $<) 2>&1 | tee $@ 1>&2
	touch --date='3 seconds' $<

$(DOWNCONF): $(MK_PROJECT_JSON) $(PROJCONF)
	node $(MK_PROJECT_JSON) $(APORT_DIR) > $@

$(UPCONF): $(MK_PROJECT_JSON) $(PROJCONF)
	node $(MK_PROJECT_JSON) $(DPORT_DIR) > $@
```

### 応用１: dport で開発
park を使わずに aport と dport だけで開発してもいいよ。

### 応用２ : ローカルでサイドバー開発
Googleスプレッドシートのサイドバーの開発、ローカルで開発できるとかなり楽。でも、リモートのコードはそのままではローカルでは動かない。でも、receive 時 (aport から park に持ってくるとき) にローカル用に変換、send 時 (park から dport に送るとき) にリモート用に変更するようにしておけば、ローカルでサクッと開発できるよ、と。

説明が面倒なんでとりあえずコードだけ載せとくよ。あとで説明を書く努力はするつもり。

```sh:receive.sh
awk -f ./receive_y16.awk ./aport/y16/sidebar.html > ./park/y16.html
```


```awk:receive_y16.awk
BEGIN {
    include("y16_header.html");
}

/this\.include/ {
    p0 = index($0, "(") + 2;
    p1 = index($0, ")") - 2
    name = substr($0, p0, p1 - p0 + 1)
    file = "aport/y16/" name ".html";
    print "<!-- @include start: " name " -->";
    include(file);
    print "<!-- @include end: " name " -->";
    next;
}

{
    myprint($0);
}

END {
    include("y16_footer.html");
}

function include(file) {
    CAT = "cat " file;
    while ((CAT | getline) > 0) {
	myprint($0);
    }
    close(CAT);
}

function myprint(line) {
    print gensub("google\\.script\\.run", "dummyGSR", "g");
}
```

```html:y16_header.html
<script>
var dummyGSR = {
  withSuccessHandler: function(func) {
    this.successHandler = func;
    return this;
  },
  withFailureHandler: function(func) {
    this.failureHandler = func;
    return this;
  },
  withUserObject: function(obj) {
    this.obj = obj;
    return this;
  },
  fillWithMaster: function() {},
  reloadMaster: function() {}
};

</script>
<div id="page">
 <div class="sheet"></div>
<!-- end of y16_header -->
```

```html:y16_footer.html
<!-- start of y16_footer -->
</div> <!-- page -->
```


```sh:send.sh
awk -f ./send_y16.awk ./park/y16.html > ./dport/y16/sidebar.html
```

```awk:send_y16.awk
BEGIN {
    silent = 1;
}

$0 == "<!-- start of y16_footer -->" {
    silent = 1;
    next;
}

$0 == "<!-- end of y16_header -->" {
    silent = 0;
    next;
}

silent == 1 {
    next;
}

$1 == "<!--" && $2 == "@include" && $3 == "start:" {
    name = $4;
    file = "dport/y16/" name ".html"
    p1 = index($0, ")") - 2
    file = "dport/y16/" name ".html";
    print "<?!= this.include('" name "') ?>";
    next;
}

$1 == "<!--" && $2 == "@include" && $3 == "end:" {
    file = "";
    next;
}

file != "" {
    print replace($0) > file;
    next;
}

{
    print replace($0);
}

function replace(line) {
    return gensub("dummyGSR", "google.script.run", "g");
}
```