6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

位置を指定して外部ファイルをMarkdownにインクルードすることで記事内のコードスニペットをテストする試行錯誤をご覧ください

Last updated at Posted at 2017-01-16

ブログなどに掲載するコードが動かないことがしばしばあるので、どうにかできないかなあと考えました。

  • コードをテストできればいいんじゃね?
  • 外部ファイルにテスト書いて、必要な部分だけインクルードすればテストできそう。

ということで、部分的にMarkdownをincludeでき(かつqiitaに投稿しちゃう)コマンドqiitap を作りました。この記事はこのような問題に僕が一人で寝る前のちょっとした時間を惜しんで取り組んだ試行錯誤の記録です。

こちらには記述方法のサンプルがあります。http://qiita.com/TakesxiSximada/items/6fa704dffd25dce635e2

このコードスニペット、動かない!?

技術系ブログにはプログラムをどう書けばいいかを説明するためにコードスニペット(コードの切れ端)がよく用いられます。コードスニペットがあると読み手はそのコードをターミナルやファイルにコピー&ペーストして動作することを手軽に確認できます。

コードスニペットの例
def hello():
    print('Hello')

ello()

しかしコードスニペットが正しく動作しないことがあります。環境の違いによるものもありますが、最も典型的なミスはコピー&ペーストした時の作業ミスで意図しない文字を挿入してしまっていたり、逆に挿入すべき文字がうまくペーストできなかったすることです。上記のコードスニペットにはコピペミスがあります。どこかわかりますか? このようなコードスニペットは当然正しく動作しません。読者は動かないコードスニペットを前にして、ただ呆然と立ち尽くし、混乱のうちにやがて朽ちて果てるでしょう。

それではあんまりですよね?

コピー&ペーストのミスで動かないとしましたが、本当にそうでしょうか?コピー&ペーストでミスをしても記事にした後でコードをテストすれば動かないことに気づきます。気付けば修正するので、そのまま掲載されているということは、気づいていない/テストしていないということになります。

ではなぜテストしないのでしょうか?それはズバリ面倒だからでしょう。記事内のコードスニペットをテストするためには、再度コピーしてターミナルやファイルにペーストしなければなりません。
コマンド一つでテストを実行というわけにはいかないので、テストは当然しないでしょう。テストをしにくい状態が出来上がっています。

コードスニペットをテストしやすくするには?

ではコードスニペットはどうすればテストしやすくなるのでしょうか? Markdown内に記載されたコードスニペットを抜き出してそれぞれの処理系で実行すれば動作の確認はできます。次の手順で処理を行うことになるでしょう。

  1. Markdownをパースする
  2. スニペット部分を抜き出す
  3. スニペット部分が何で書かれているかを判定する
  4. 処理系にコードスニペットを流す

気が遠くなりますね。心が折れてしまいます。

他の問題もあります。テストコードにはコードスニペットとして表示したくない箇所があります。例えばMockオブジェクトの作成や、データ作成のための前処理、assertなどの結果の確認です。これらはテストコードが正しく動いているかを確認するためのものなので記事上に掲載すると読者はますます混乱してしまいます。

ではテストコードを外部のファイルに記述し、記事用のMakrdownにはそれを読み込むためのinclude命令を追加するのはどうでしょう。コードスニペットはそれぞれの処理系のユニットテストの記述方法で記述することで、テストはし易くなるでしょう。通常のincludeの機能のようにファイルの中身全てをincludeしてしまうと、Mockオブジェクトの作成や、データ作成のための前処理、assertなどの結果の確認 まで表示してしまうことになるので、includeするファイルと位置が指定できると良さそうです。

Sphinxのliteralincludeディレクティブは範囲を指定してincludeできる

Python系のプロジェクトでよく使われるドキュメンテーションツールSphinxにはliteralincludeというディレクティブがあります。これはディレクティブを記述している箇所に、引数として指定したファイルの内容を挿入します。しかもliteralincludeは挿入する範囲を:start-after::end-before:をパラメータで絞ることができます。例えば次のテストコードtest_it.pyがあるとします。

test_it.py
import unittest


class SimpleTest(unittest.TestCase):
    def test_it(self):
        # [START]
        import json
        text = json.dumps({})
        # [END]
        self.assertEqual('{}', text)

このファイルはテストコードなので、全てをドキュメントに含めたくはありませんが、[START][END] で挟まれたコードは実際の使い方を表しているので、ドキュメントに含めたいです。Sphinxのliteralincludeディレクティブを使うと、この箇所だけを抜き出してドキュメントに含められます。:start-after: に開始位置を示す文字列を、:end-before: に終了位置を示す文字列を指定します。

literalincludeの例
.. literalinclude:: ./test_it.py
   :start-after: [START]
   :end-before: [END]

rstファイルはこのようになります。

index.rst
Welcome to tes's documentation!
===============================

.. toctree::
   :maxdepth: 2
   :caption: Contents:

.. literalinclude:: ./test_it.py
   :start-after: [START]
   :end-before: [END]

Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

index.rstをHTMLに変換するとこのようなページが生成されます。そしてtest_it.pyははユニットテストとしても正しく動作します。

ユニットテストを実行する
$ python -m unittest
.
----------------------------------------------------------------------
Ran 1 test in 0.084s

OK

Sphinxは良いツールなのですが、SphinxでRestructuredTextをビルドするにはconf.py作らないといけないし、標準出力に変換後のMarkdownを出力してパイプでつなぐ処理もやりづらいので今回はSphinxを使うのを諦めました。

じゃあどうするのか?

Markdownの処理ツールによってはincludeをサポートしているものもあります。HugoのPartialなどはそれに当たるでしょう。今回はQiitaに投稿することを前提としているのでHTMLではなくMarkdownで出力してほしいのです。ということはMarkdownの処理系ではなくただのテンプレートエンジンで用件は満たせそうです。また特定のタグに挟まれた箇所だけを抜き出したいのでそのようなinclude処理を簡単に変更できるテンプレートエンジンが望ましいです。現時点で使い慣れているテンプレートエンジンだとMakoとJinja2でした。Makoの方がより自由度の高いテンプレートエンジンのため、今回はMakoを使ってMarkdownの前処理をします。

Makoでなんとかする

Mako はPythonでよく使われるテンプレートエンジンの一つです。どうでもいいですがGoogleでMakoを検索すると声優さんのWikipediaのページがトップに出てきますが、それは関係ありません。

Makoのinclude機構

Makoにはinclude機構があります。以下はincludeタグの簡単な使い方の例です。

<%include file='test_sphinx/test_it.py' />

このようにincludeタグを使うことでファイルをインクルードできます。レンダリングは以下のようにします。


from mako.tempate import Template
with open('/path/to/your/markdown', 'rb') as fp
    tmpl = Template(fp.read())
markdown_content = tmpl.render()

markdown_content には前処理されたMardownの文字列が格納されます。ただしこのincludeタグは開始位置と終了位置を指定できません。

通常のincludeの機能のようにファイルの中身全てをincludeしてしまうと、Mockオブジェクトの作成や、データ作成のための前処理、assertなどの結果の確認 まで表示してしまうことになるので、includeするファイルと位置が指定できると良さそうです。

前述したようにファイルと開始位置と終了位置を指定したいので、指定可能なカスタムタグを作成します。

開始/終了位置が指定できるカスタムincludeタグを作成する

MakoではカスタムタグはPythonで書きます。callableなオブジェクトであればカスタムタグとして使えます。第一引数には必ずファイルライクオブジェクトが渡されます。それ以外の引数は任意で使えます。そのため、第一引数にcontext、第二引数に開始位置を示す文字列、第三引数に終了位置を示す文字列をわたすようにします。

開始位置と終了位置を探すには1行ずつなめるようにしました。

with open(path) as source:
    for ii, line in enumerate(source):
        if not is_content and start_at in line:
            is_content = True
            if line != '' and line[0] in (' ', '\t'):
                truncate_char = line[0]
                truncate_count = len(line) - len(line.lstrip(truncate_char))
        elif is_content and end_at is not None and end_at in line:
            is_content = False
            break
        elif is_content:
            fp.write(line)

この時、開始位置の左側の空白の数を数えています。これは後でその空白を削除して左寄せするためです。以下では左寄せを行なっています。

for line in fp:
    truncate_line = line[:truncate_count] if truncate_char else line
    writable_line = line[truncate_count:]
    if truncate_char:
        if line.strip() == '':
            writable_line = '\n'
        elif len(truncate_line) != truncate_line.count(truncate_char):
            raise TruncateError(
                'Truncate failed: len={}, count={}: start={}, end={}: {}'.format(
                    len(truncate_line), truncate_line.count(truncate_char),
                    start_at, end_at, repr(truncate_line)))
    context.write(writable_line)

カスタムタグを定義したファイルはこのようになります。

custom_tag.py
import six

if six.PY2:
    from StringIO import StringIO
else:
    from io import StringIO


class TruncateError(Exception):
    pass


def custom_include(context, path, start_at='', end_at=None):
    truncate_count = 0
    truncate_char = ''

    fp = StringIO()
    is_content = False

    # [POSTION_INCLUDE_START]
    with open(path) as source:
        for ii, line in enumerate(source):
            if not is_content and start_at in line:
                is_content = True
                if line != '' and line[0] in (' ', '\t'):
                    truncate_char = line[0]
                    truncate_count = len(line) - len(line.lstrip(truncate_char))
            elif is_content and end_at is not None and end_at in line:
                is_content = False
                break
            elif is_content:
                fp.write(line)
    # [POSTION_INCLUDE_END]

    fp.seek(0)

    # [LEFT_START]
    for line in fp:
        truncate_line = line[:truncate_count] if truncate_char else line
        writable_line = line[truncate_count:]
        if truncate_char:
            if line.strip() == '':
                writable_line = '\n'
            elif len(truncate_line) != truncate_line.count(truncate_char):
                raise TruncateError(
                    'Truncate failed: len={}, count={}: start={}, end={}: {}'.format(
                        len(truncate_line), truncate_line.count(truncate_char),
                        start_at, end_at, repr(truncate_line)))
        context.write(writable_line)
    # [LEFT_END]
    return ''

では次のサンプルファイルをカスタムタグで読み込んでみましょう。

fish.txt(サンプルテンプレート)
AAAAAAAAAAAAAA
AAAAAAAAAAAAAA
AAAAAAAAAAAAAA
    [START]
    BBBBBBBBBBBBB
    BBBBBBBBBBBBB
       BBBBBBBBBBBBB
       BBBBBBBBBBBBB
    BBBBBBBBBBBBB
    BBBBBBBBBBBBB
    [END]
AAAAAAAAAAAAAA
AAAAAAAAAAAAAA

カスタムタグを使う場合は以下のようにします。

カスタムタグの使用例
<%namespace name="custom_tag" module="custom_tag"/>


${custom_tag.custom_include("./fish.txt", "[START]", "[END]")}

出力はこのようになります。

レンダリング結果
BBBBBBBBBBBBB
BBBBBBBBBBBBB
   BBBBBBBBBBBBB
   BBBBBBBBBBBBB
BBBBBBBBBBBBB
BBBBBBBBBBBBB

良さそうです。

#マークが消える?

Makoでレンダリングしてたら、見出しが消えていることに気がつきました。

消えた見出し
### これとか
#### これとか
##### これとか

一方消えなかった見出しもありました。

消えなかった見出し
# これは消えなかった

どうやらh2以上の見出しが消えてしまっていました。理由は簡単で ## はMakoがコメントとして解釈していました。そのためレンダリングされず見出しが消えてしました。コメントの書き方は ##<%doc></%doc> を使う書き方があります。後者の方は使いたいケースもあると思うので、## だけをコメントとして解釈しないようにMakoの挙動を変更します。

Makoのトークン解析はmako.lexer.Lexer()で実装されています。制御要素はmako.lexer.Lexer.match_control_line()で解析されています。実際にこのメソッドの先頭にそれっぽい正規表現が見て取れます。

制御要素を解析するための正規表現
    def match_control_line(self):
        match = self.match(
            r"(?<=^)[\t ]*(%(?!%)|##)[\t ]*((?:(?:\\r?\n)|[^\r\n])*)"
            r"(?:\r?\n|\Z)", re.M)        match = self.match(

ぱっと見たところ ## が正規表現に記述されています。明らかにこれなので消してしまえば良さそうです。しかし正規表現はメソッド内部で記述されているため、メソッドをコピーして実装するしか手はなさそうです(悲しい)。MarkdownLexerを新たに定義してmatch_control_lineメソッドをoverrideしてしまいます。

MarkdownLexerクラス
class MarkdownLexer(Lexer):
    def match_control_line(self):
        # <MATCH_CONTROL_RE>
        match = self.match(
            r"(?<=^)[\t ]*(%(?!%))[\t ]*((?:(?:\\r?\n)|[^\r\n])*)"
            r"(?:\r?\n|\Z)", re.M)
        # </MATCH_CONTROL_RE>

        if match:
            operator = match.group(1)
            text = match.group(2)
            if operator == '%':
                m2 = re.match(r'(end)?(\w+)\s*(.*)', text)
                if not m2:
                    raise exceptions.SyntaxException(
                        "Invalid control line: '%s'" %
                        text,
                        **self.exception_kwargs)
                isend, keyword = m2.group(1, 2)
                isend = (isend is not None)

                if isend:
                    if not len(self.control_line):
                        raise exceptions.SyntaxException(
                            "No starting keyword '%s' for '%s'" %
                            (keyword, text),
                            **self.exception_kwargs)
                    elif self.control_line[-1].keyword != keyword:
                        raise exceptions.SyntaxException(
                            "Keyword '%s' doesn't match keyword '%s'" %
                            (text, self.control_line[-1].keyword),
                            **self.exception_kwargs)
                self.append_node(parsetree.ControlLine, keyword, isend, text)
            else:
                self.append_node(parsetree.Comment, text)
            return True
        else:
            return False

変更点は ## を正規表現から抜いただけです。

修正した正規表現
match = self.match(
    r"(?<=^)[\t ]*(%(?!%))[\t ]*((?:(?:\\r?\n)|[^\r\n])*)"
    r"(?:\r?\n|\Z)", re.M)

このmako.template.Template()はコンストラクタの引数としてLexerオブジェクトのファクトリを渡すことができます(いいですねえ)。以下は実際のレンダリングの例です。

レンダリングの例
lookup = TemplateLookup(directories=['.'])
tmpl = Template(body, lookup=lookup, lexer_cls=MarkdownLexer)
renderd_body = tmpl.render(attr=attributes)

Qiitaに記事を投稿する

生成したMakrdownをQiitaに投稿する

QiitaはAPI経由で記事を投稿できます。その気になればcurlを使って投稿もできます。新規投稿に関しては Qiita API v2ドキュメント - Qiita:Developer に仕様が記載されています。どうやら以下のようなbodyを送信する必要がありそうです。

新規投稿のbody
{
  "body": "# Example",
  "coediting": false,
  "gist": false,
  "private": false,
  "tags": [
    {
      "name": "Ruby",
      "versions": [
        "0.0.1"
      ]
    }
  ],
  "title": "Example title",
  "tweet": false
}

Qiita API v2ドキュメント - Qiita:Developer を抜粋

bodyとtitleは指定する必要がありそうですが、他のパラメータは後で考えれば良さそうです。試しにcURLでPOSTしてみます。

新規投稿でUnauthorized
$ curl -X POST https://qiita.com/api/v2/items --data @create.json
{"message":"Unauthorized","type":"unauthorized"}%

Unauthorized になってしまいました。どうやら認証処理が必要なようです。それはそうですよねえ。Qiita API v2ドキュメントの認証認可 には以下のようにあります。

アクセストークン

アクセストークンは、以下のようにAuthorizationリクエストヘッダに含められます。
Authorization: Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd

Qiitaの管理画面からアクセストークンを発行し、それをHTTPヘッダに含める必要がありそうです。アクセストークンは https://qiita.com/settings/applications で発行できます。発行したアクセストークンを使ってQiitaに投稿してみます。

新規投稿でInvaliContentType
$ curl -X POST https://qiita.com/api/v2/items --data @create.json -H "Authorization: Bearer MASKMASKMASKMASKMASKMASKMASKMASK"
{"message":"Invalid content type","type":"invalid_content_type"}%

Invalid content type になってしまいました。bodyがJSONなのでapplication/jsonにするべきでしたね。Content-Type ヘッダを追加して再度投稿します。

curl -X POST https://qiita.com/api/v2/items --data @create.json -H "Authorization: Bearer MASKMASKMASKMASKMASKMASKMASKMASK" -H "Content-Type: application/json"
{"rendered_body":"\n\u003ch1\u003e\n\u003cspan id=\"example\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#example\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003eExample\u003c/h1\u003e\n","body":"# Example\n","coediting":false,"created_at":"2017-01-21T16:34:03+09:00","group":null,"id":"xxxxxxxxxxxxxxx","private":false,"tags":[{"name":"Ruby","versions":["0.0.1"]}],"title":"Example title1","updated_at":"2017-01-21T16:34:03+09:00","url":"http://qiita.com/TakesxiSximada/items/4d12097001d4ab8983c6","user":{"description":"","facebook_id":"","followees_count":2,"followers_count":21,"github_login_name":"TakesxiSximada","id":"TakesxiSximada","items_count":38,"linkedin_id":"","location":"","name":"Takesxi Sximada","organization":"","permanent_id":36261,"profile_image_url":"https://qiita-image-store.s3.amazonaws.com/0/36261/profile-images/1473687011","twitter_screen_name":"TakesxiSximada","website_url":""}}%

レスポンスを見やすくしてみます。

成形済みレスポンス
{
  "rendered_body": "\n<h1>\n<span id=\"example\" class=\"fragment\"></span><a href=\"#example\"><i class=\"fa fa-link\"></i></a>Example</h1>\n",
  "body": "# Example\n",
  "coediting": false,
  "created_at": "2017-01-21T16:34:03+09:00",
  "group": null,
  "id": "xxxxxxxxxxxxxxxxxxxxxxx",
  "private": false,
  "tags": [
    {
      "name": "Ruby",
      "versions": [
        "0.0.1"
      ]
    }
  ],
  "title": "Example title1",
  "updated_at": "2017-01-21T16:34:03+09:00",
  "url": "http://qiita.com/TakesxiSximada/items/4d12097001d4ab8983c6",
  "user": {
    "description": "",
    "facebook_id": "",
    "followees_count": 2,
    "followers_count": 21,
    "github_login_name": "TakesxiSximada",
    "id": "TakesxiSximada",
    "items_count": 38,
    "linkedin_id": "",
    "location": "",
    "name": "Takesxi Sximada",
    "organization": "",
    "permanent_id": 36261,
    "profile_image_url": "https://qiita-image-store.s3.amazonaws.com/0/36261/profile-images/1473687011",
    "twitter_screen_name": "TakesxiSximada",
    "website_url": ""
  }
}

投稿できました。適当に作成したcreate.jsonを用いましたが、これをMarkdownから生成すれば良さそうです。

titleを記事内に埋め込む

Sphinxでは以下のような形式でタイトルを指定できます。

Sphinxでページタイトルを指定する例
.. title:: Page title

Pelicanでは以下のような形式でタイトルを指定できます。

Pelicanでページタイトルを指定する例
Title: My super title

Jekyllでは以下のような形式でタイトルを指定できます。

Jekyllでページタイトルを指定する例
---
title: Oh! My JK
---

辛いです。そのままQiitaに投稿してしまうとページに表示されてしまいます。ページには表示して欲しくないので、Mako Styleの最初のコメント要素にYAML形式でタイトルを保持するようにしました。

タイトルをMarkdown内に記述する例
<!--- title: [WIP] 記事内のコードスニペットをテストする --->

タイトルの解析は別途やることにしました。

タイトルを取得する正規表現
title_regx = re.compile(r'<!--- title:(?P<title>.*) --->')
タイトルを取得する処理
title_line = args.source.readline()
matching = title_regx.search(title_line)
title = matching.group('title').strip() if matching else 'NO TITLE'

先頭の1行決め打ちでタイトルとかダサいので先頭のコメントに属性を設定するように修正

上記の実装だと先頭の1行しかタイトルの設定はできません。しかも他の属性を設定することもできないので拡張に乏しい実装です。あまり良くないので実装し直します。Makoで解析した時の先頭のコメント要素にYAML形式で属性値を記述することにしました。

属性設定の例
<%doc>
  title: "[WIP] 記事内のコードスニペットをテストする"
  item_id: "dba9c0ab96311ab4c0c3"
</%doc>

この属性はファイルの先頭(正確にはファイル内のコメント要素の1個目)でなければなりません。レンダリング処理とは別にLexerを使ってファイルをパースして、最初のコメント要素を取得しています。中の文字列はYAML(辞書)で記述されていることを前提にしていて、YAMLパーサで解釈されます。

属性値を取得する処理
def get_attributes(body):
    lexer = MarkdownLexer(body)
    tmpl = lexer.parse()
    for node in tmpl.nodes:
        if isinstance(node, mako.parsetree.Comment):
            return yaml.safe_load(node.text)
    return {}

こうすることでtitle以外も属性を設定可能になりました。またrender()にこのオブジェクトを渡すことでMarkdown内でも属性値を使えるようになっています。

Qiita記事の更新用JSONを生成する

記事の投稿と同様、APIを使って記事を更新できます。API仕様はQiita API v2ドキュメント - Qiita:Developer に記載されています。

MarkdownをレンダリングしてJSONSerializableなオブジェクトを返す
attributes = get_attributes(body)
title = attributes.get('title')
if not title:
    raise NoTitleError()

# <RENDER>
lookup = TemplateLookup(directories=['.'])
tmpl = Template(body, lookup=lookup, lexer_cls=MarkdownLexer)
renderd_body = tmpl.render(attr=attributes)
# </RENDER>

return {
    "body": renderd_body,
    "coediting": False,
    "gist": False,
    "private": False,
    "tags": [
        {
            "name": "Qiita",
            "versions": [
                "0.0.1"
            ],
        },
    ],
    "title": title,
}

この処理を使うと次のようなJSONを生成できます。

生成されるJSON
$ python qiita_json.py index.md | jq
{
  "body": "\n\n\n\n# このコードスニペット、動かない!?\n\n技術系ブログにはプログラムをどう書けばいいかを説明するためにコードスニペット(コードの切れ端)がよく用いられます。コードスニペットがあると読み手はそのコードをターミナルやファイルにコピー&ペーストして動作することを手軽に確認できます。\n\n```py:コードスニペットの例\ndef hello():\n    print('Hello')\n\nell

  ~~~  省略 ~~~

  py3:MarkdownをレンダリングしてJSONSerializableなオブジェクトを返す\nattributes = get_attributes(body)\ntitle = attributes.get('title')\nif not title:\n    raise NoTitleError()\n\n# <RENDER>\nlookup = TemplateLookup(directories=['.'])\ntmpl = Template(body, lookup=lookup, lexer_cls=MarkdownLexer)\nrenderd_body = tmpl.render(attr=attributes)\n# </RENDER>\n\nreturn {\n    \"body\": renderd_body,\n    \"coediting\": False,\n    \"gist\": False,\n    \"private\": False,\n    \"tags\": [\n        {\n            \"name\": \"Qiita\",\n            \"versions\": [\n                \"0.0.1\"\n            ]\n        }\n    ],\n    \"title\": title,\n}\n\n```\n",
  "coediting": false,
  "gist": false,
  "private": false,
  "tags": [
    {
      "name": "Qiita",
      "versions": [
        "0.0.1"
      ]
    }
  ],
  "title": "[WIP] 記事内のコードスニペットをテストする"
}

このJSONをcURLを使って更新してみます。

cURLで記事を更新
python qiita_json.py index.md | curl -X PATCH https://qiita.com/api/v2/items/dba9c0ab96311ab4c0c3 --data @- -H "Authorization: Bearer MASKMASKMASKMASKMASKMASK" -H "Content-Type: application/json"

更新できましたか?記事更新はPOSTではなくてPATCHでリクエストを送信するのがポイントです。

CLIツールまで実装する (最後の方、投げやり)

必要な機能はこれまでで大体実装できました。ただパッケージ化してCLIツールを書くまでが遠足です。最後までちゃんとやりましょう。

仕様はこんな感じ?

  • 認証機能: auth
    • ブラウザを開く
    • トークンを生成する
    • ~/.qiitap/token にトークンを保持する
  • 記事作成機能: create [FILEPATH] [--title FILEPATH] [--public] [--tweet] [--gist] [--tag]
    • 新規に記事を作成する
    • FILEPATHにテンプレートMarkdownを生成する
      • ヘッダーに記事IDを挿入する
      • ヘッダーにタイトルを挿入する
      • ヘッダーにタグを挿入する
  • 記事更新機能: update [FILEPATH]
    • ヘッダーの情報を元に記事を更新する

qiitap

ということで作りました。その名もqiitapです。

インストール

まずインストールします。

$ pip install qiitap

qiitapコマンドが使えるようになります。

Token情報などを入力

Token発行ページをbrowserでひらき、Token情報などを聞いてくるので入力します。

$ qiitap auth

聞いてくる項目は2つです。

  • API Token
  • デフォルトで使うタグ

--no-browserを指定するとブラウザを開きません。

記事の新規作成

記事を作成します。

$ qiitap create

qiita上には限定共有投稿で記事が作られ、ローカルにはqiita.md が生成されます。

記事の公開

記事を編集し公開します。qiita.mdprivate: TrueなっていることろをFalseに書き換えます。

qiita.md
private: False

記事を更新します。

$ qiitap update

更新されましたか?現在はPython3にしか対応してません。多分2への対応はしないと思う(だるいから)。こんな誰得感あるもののために結構時間使ってしまったなあ。寝る前のちょっとした時間を大幅にオーバーしてしまった。もう、疲れたよ。

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?