Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

golang.org/x/sync/semaphoreを使ってゴルーチンの同時実行数を制御する

この記事はQiitaで公開されていました

特定の処理をゴルーチンで並列実行したいけれど、サーバの負荷等を考慮して、同時実行数の上限を設定したい話です。元ネタの記事では、チャネルやsync.Poolを使って実現していて、すでに十分シンプルなのですが、x/sync/semaphoreを使う方法も便利だったので紹介します。

見た目はほぼ、チャネルを使った実装と同じですが、s.Acquire(ctx, n)nの値で重みをつけることができます。なので、Aという処理が動いているときは他の処理を行わない、けれどBなら3個まで同時に動いても良い、といった対応をチャネルで行うと面倒ですが、semaphore.Weightedなら重みを変更するだけで実現できるので便利だと思いました。

元ネタ

実装例

以下の例は、同時実行数が3つに制限された状態でdoSomething(u)を並列実行します。サンプルコード自体はmattnさんのものをほぼそのまま流用しました。

全ての処理完了を待つためにsync.WaitGroupを使っていますが、semaphore.Weightedには全く関係ありません。

2024-04-10追記 sync.WaitGroup を使わなくても、最後にs.Acquire(Limit)すると完了を待てるので、そっちのほうがシンプルですね。

package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/semaphore"
)

func doSomething(u string) {
    fmt.Println(u)
    time.Sleep(2 * time.Second)
}

const (
    Limit  = 3 // 同時実行数の上限
    Weight = 1 // 1処理あたりの実行コスト
)

func main() {
    urls := []string{
        "http://www.example.com",
        "http://www.example.net",
        "http://www.example.net/foo",
        "http://www.example.net/bar",
        "http://www.example.net/baz",
    }
    s := semaphore.NewWeighted(Limit)
    var w sync.WaitGroup
    for _, u := range urls {
        w.Add(1)
        s.Acquire(context.Background(), Weight)
        go func(u string) {
            doSomething(u)
            s.Release(Weight)
            w.Done()
        }(u)
    }
    w.Wait()
}

説明

まずは、semaphore.NewWeighted(lim)lim個のリソースをもつsemaphore.Weightedを作成します。s.Acquire(ctx, n)は、全体のリソース(lim)からn個消費しますが、Weightedのリソースが足りない場合は、s.Acquire(ctx, n)の呼び出しは他のゴルーチンからs.Release(n)されるまでブロックします。そのため、同時にlim個以上の処理が動くことはありません。

処理が終わった後s.Release(n)を使うと、n個のリソースをWeightedへ戻します。ブロックしていたs.Acquire(ctx, n)があれば、ブロックが解除されて続きの処理を行います。

また、s.TryAcquire(n)というメソッドも用意されていて、こちらはブロックしません。代わりに、リソースが取得できたらtrueを返します。

Jenkinsfileの書き方

この記事はQiitaで公開されていました

ほとんど情報がなかったので調べながら書いたメモ。基本的に、公式情報が最もまとまっている。

また、古い情報らしいけど、現状こちらにしか書かれていない項目もある。

Declarative Pipelineの1.2.8から、ジェネレータが追加されたようです。

基本

現状のJenkinsfileは2通り書き方があって、pipelineがルートになっている場合はDeclarative Pipelineという。この場合は、Groovyスクリプトを直接書くことができず、Groovyを書きたければscriptディレクティブを使う必要がある。この記事では主にこちらを扱う。

pipelineから始まらない場合、Scripted Pipelineといって、この場合は直接Groovyスクリプトも書けるし、node()stage()などの、Pipeline Stepsメソッドも書くことができる。便利そうにみえるけど自由度が高すぎて職人コードになりがち。

Declarative Pipelineの書き方

Jenkinsfileは特定の条件ごとにディレクティブを書いて実装する。ディレクティブの出現順は以下の順番になっている。例えばstagesのすぐ下にagentを書くことはできない。

pipeline {
    // 1行コメント
    /*
    * 複数行コメント
    */
    agent { ... }
    environment { ... }
    options {
        buildDiscarder(...)
        checkoutToSubdirectory(...)
        disableConcurrentBuilds(...)
        disableResume()
        newContainerPerStage(...)
        overrideIndexTriggers(...)
        preserveStashes(...)
        quietPeriod(...)
        retry(...)
        skipDefaultCheckout(...)
        skipStagesAfterUnstable(...)
        timeout(...)
        timestamps()
        parallelsAlwaysFailFast(...)
    }
    parameters {
        string(...)
        booleanParam(...)
        choice(...)
        file(...)
        text(...)
        password(...)
        run(...) // ??
    }
    tools {
        maven '...'
        jdk '...'
        gradle '...'
    }
    triggers {
        cron(...)
        pollSCM(...)
    }
    stages {
        stage {
            agent { ... }
            environment { ... }
            tools { ... }
            options {
                skipDefaultCheckout(...)
                timeout(...)
                retry(...)
                timestamps()
            }
            when { ... }
            steps {
                //Pipeline Steps Reference参照
                echo 'help'
                sh 'ls'
                script {
                    // 任意のGroovyスクリプト
                }
            }
        }
    }
    steps {
        //Pipeline Steps Reference参照
    }
    post {
        always { ... }
        success { ... }
        failure { ... }
        ...
    }
}

stepsだけの場合は、直接stepsの中に書くものを記述できる。

echo 'help'
sh 'ls'

node()のようにstepsディレクティブの中で子ブロックが現れる場合、そのブロックの中はstepsディレクティブと同じものが書ける。

steps {
    node('slave1'){
        sh 'ls'
        dir('output'){
            sh 'ls'
        }
    }
}

個別の事例

stageの書き方

stage()を使うと、進捗を分けて表示することができるが、インターネットでは2種類の書き方がある。stage単体で書かれているものは古く、推奨されない。

stage 'first'
sh 'ls'

stage 'second'
sh 'pwd'

今はブロックを取るようになっていて、こちらが推奨される。

stage('first'){
    sh 'ls'
}
stage('second'){
    sh 'pwd'
}

ビルドするノードを制限する

agentディレクティブでラベルを指定する。

agent {
    label 'slave1'
}

または、stepsディレクティブでnodeブロックを使う。

steps {
    node('slave1'){
    }
}

環境変数をセットする

environmentディレクティブが使える場合はその中で書く。ここでは、定数またはcredentials()しか使えない。また、credentials()SSHユーザ名と秘密鍵を扱えない。

environment {
    GOPATH = '/home/jenkins/go'

    // Jenkinsの資格情報に登録されている値を取り出す
    TOKEN = credentials('credential_id')
}

credentials()で取り出したものは、シークレットテキストの場合はそのまま使える。ユーザ名とパスワードの場合、TOKENはユーザ名とパスワードを:で区切った文字列になっている。個別に使いたい場合は、TOKEN_USRTOKEN_PSWのようにサフィックスを付けて扱うと良い。

または、stepsディレクティブの中でwithEnvブロックやwithCredentialsブロックを使う。withEnvの中でだけ環境変数が有効になる。

steps {
    withEnv(["GOPATH=${env.WORKSPACE}"]) {
    }
    withCredentials(bindings: [
        // シークレットテキスト
        string(credentialsId: 'credential_id', variable: 'TOKEN'),

        // ユーザ名とパスワード
        usernamePassword(credentialsId: 'credential_id',
         passwordVariable:'PASS',
         usernameVariable:'USER'),

        // ファイル
        file(credentialsId: 'credential_id', variable: 'FILE'),

        // SSH秘密鍵: passphraseVariable, usernameVariableは省略可
        sshUserPrivateKey(credentialsId: 'credential_id',
         keyFileVariable: 'KEY_FILE',
         passphraseVariable: '',
         usernameVariable: '')
    ]){
    }
}

withCredentials()の詳細はCredentials Binding Pluginを読むと良い。

Groovyを使ってもっと細かく制御したい場合。stepsディレクティブに直接Groovyは書けないのでscriptディレクティブを使う。

steps {
    script {
        // ここからGroovy
        env.GOPATH = env.WORKSPACE
    }
}

ツールをインストールする

Jenkinsの管理メニューにGlobal Tools Configuration があり、そこで事前にMavenやGoコンパイラなどのバージョンと名前を定義しておくと、toolsディレクティブで、自動でインストールすることができる。

pipeline {
    tools {
        go 'go1.10'
    }
}

Pipeline Syntaxで自動生成したコードはtool name: 'go1.9', type: 'go'のように出力されるが、このままDeclarative Pipelineとして記述するとエラーになる。$type '$name'のように置き換えて書く必要がある。

パラメータ付きビルドにする

parametersに何か書くと、パラメータ付きビルドとして扱われる。params.nameで参照する。

pipeline {
    parameters {
        string(name: 'TARGET', defaultValue: 'Tests', description: '説明')
        choice(name: 'STAGE', choices: 'staging\nproduction', description: '説明')
    }
    stages {
        stage('build'){
            sh "./make.bash ${params.TARGET}"
        }
    }
}

実行条件を設定する

stage()ブロックでwhenディレクティブを使う。以下の例は、ブランチ名がrelease-*にマッチした場合のみstageを実行する。

stage('stage'){
    when {
        branch 'release-*'
    }
}

他にも、環境変数の値を条件としたり、全て一致またはどれかが一致などの条件を作ることができる。

複数行のシェルスクリプトを実行

'''または"""で囲むと複数行のテキストを記述できる。

steps {
    sh '''
  date
  sleep 10
  date
  '''
}

'''の場合は変数展開などをせずにそのままシェルへ渡す。"""なら${env.PATH}のような展開を行う。

GitHub Branch Source Plugin環境でスレーブを使ってビルドする

基本的には何もしなくてもプラグインがcloneするが、デフォルトでは(おそらく)Jenkinsマスターのワークスペースへcloneされる。このため、ビルドノードがマスター以外の場合、ビルドするソースコードがスレーブのワークスペースにない状態となる。

stepsディレクティブでcheckoutを使うとslaveのワークスペースへcloneが行われる。

steps {
    checkout scm
}

または最初から、agentディレクティブでラベルを指定しておくと、対象となったスレーブでcloneされる。

pipeline {
    agent {
        label 'slave1'
    }
    steps {
        sh 'ls'
    }
}

特定のディレクトリへcloneする

optionsディレクティブで指定する。

options {
    checkoutToSubdirectory 'src/v2'
}

または、dir()を使うとカレントディレクトリを変更できるので、移動した後でcheckout scmを実行すればいい。移動するディレクトリが存在しない場合は自動的に作成される。

stage('chekout'){
    steps {
        dir("src/v2"){
            checkout scm
        }
    }
}

GitHub Branch Source Pluginでcheckoutの動作を変更する

How to Customize Checkout for Pipeline Multibranchによると、checkout scmは、

checkout([
 $class: 'GitSCM',
 branches: scm.branches,
 extensions: scm.extensions,
 userRemoteConfigs: scm.userRemoteConfigs
])

の省略形らしい。Shallow cloneとかcloneするディレクトリを変更する場合は、scm.extensionsに配列でオプションを追加する。

checkout([
 $class: 'GitSCM',
 branches: scm.branches,
 extensions: scm.extensions + [
        [ $class: 'CloneOption', noTags: false ],
        [ $class: 'RelativeTargetDirectory',
       relativeTargetDir: "src/v2"
        ],
    ],
 userRemoteConfigs: scm.userRemoteConfigs
])

Gitサブモジュールを使う

checkout scmSubmoduleOptionをセットします。

checkout([
 $class: 'GitSCM',
 branches: scm.branches,
 extensions: scm.extensions + [
        [ $class: 'SubmoduleOption',
       disableSubmodules: false,
       parentCredentials: true,
       recursiveSubmodules: true,
       reference: '',
       trackingSubmodules: false
        ],
    ],
 userRemoteConfigs: scm.userRemoteConfigs
])

ただし、GitHub Branch Source Pluginを使う場合、parentCredentialsGitHub APIトークンが使われているため、サブモジュールの参照もHTTPで行う必要があります。

checkoutの前にワークスペースをクリアする

checkout scmの前にdeleteDirを使う。

stage('clean'){
    steps {
        deleteDir()
    }
}
stage('checkout'){
    steps {
        checkout scm
    }
}

Scripts not permitted to use methodエラーで動作しない

Jenkinsfileのコードを実行した時、

RejectedAccessException: Scripts not permitted to use method (メソッド名)

というエラーで停止する場合がある。これは、外部のコードを無条件に実行すると危険なので、Script Security Pluginによってsandbox実行されているため、らしい。

外部のコードを制限するための機能なので、Jenkinsfileで回避できるものではない。エラーが発生した後であれば、Jenkinsの管理画面にエラーとなったメソッドの許可を求めるログが出ているので、そこでApprovalボタンを押せば次からはエラーを回避できる。このファイルは$JENKINS_HOME/scriptApproval.xmlに置かれているので、これをコピーしてもいい。

成果物を保存する

postセクションでArchive Artifact Pluginを使えばよい。

pipeline {
    post {
        success {
            archiveArtifacts artifacts: bin/*, fingerprint: true
       }
   }
}

成果物の保存数を制限する

いろいろ書き方はあるが、おそらくoptionsディレクティブを使うのが簡単。

pipeline {
    options {
        buildDiscarder(logRotator(numToKeepStr: '5', daysToKeepStr: '7', artifactNumToKeepStr: '5'))
    }
}

並列ビルドする

stepsparallelを使う。

steps {
    parallel(
     linux: {
            sh './make.bash -t linux_amd64'
        },
     windows: {
            sh './make.bash -t windows_amd64'
        }
    )
}

この例では、./make.bash -t linux_amd64./make.bash -t windows_amd64が並列実行される。

他のジョブをビルドする

buildを使う。

引数のパラメータはJenkinsを置いているフォルダまでのパスを渡す。相対パスの場合は呼び出し元ジョブのディレクトリがカレントになり、絶対パスの場合は$JENKINS_HOME/jobsがルートになる。

steps {
    build '../jobName1'
}

マルチブランチ構成のプロジェクトを呼び出す場合は、内部の階層がブランチごとに切られているので、ブランチ名も必要。

steps {
    build '../jobName1/master'
}

GitHub Branch Sourceが管理しているジョブはマルチブランチに近くて、Organizationの下にジョブが作られるので、次のようになる。

steps {
    build '/organizationName/jobs/jobName/master'
}

Jenkinsが管理しているCredentialでssh接続したい

ssh-agentプラグインをインストールするとsshagentブロックが利用可能になる。このブロックの中に記述したコマンドは、Jenkinsが管理している秘密鍵が追加されたssh-agentが動作してる状態で実行される。なのでJenkinsのアカウントでgit pushしたい場合は、以下のように書く。

steps {
    // jenkins_credential_id_strという名前のCredentialを読み込む
    sshagent(['jenkins_credential_id_str']){
        sh 'git push'
    }
}

bashのあまり知られていないけど便利な話

この記事はQiitaで公開されていました

~*を展開する場合はクオートしない

クオートされた文字列の中にある~はそのまま文字として扱われる。扱いは*と同じようにみえる。

$ echo ~/bin
/Users/kadota/bin
$ echo "~/bin"
~/bin

$ echo *.txt
hello.txt
$ echo "*.txt"
*.txt

$*$@の違い

基本的には同じ動きをするが、クオートの中で使うと文字区切りの扱いが変わる。

# star.sh
for i in $*; do echo "'$i'"; done

# star-quote.sh
for i in "$*"; do echo "'$i'"; done

# atom.sh
for i in $@; do echo "'$i'"; done

# atom-quote.sh
for i in "$@"; do echo "'$i'"; done

スペース($IFS)を含む場合の例。

$ bash star.sh 1 "2 3" 4
'1'
'2'
'3'
'4'
$ bash atom.sh 1 "2 3" 4
'1'
'2'
'3'
'4'
$ bash star-quote.sh 1 "2 3" 4
'1 2 3 4'
$ bash atom-quote.sh 1 "2 3" 4
'1'
'2 3'
'4'

間接参照

rcでは変数の値を変数名として使うと便利だけど

% a=foo
% foo=1
% echo $$a
1

bashはそのまま実行すると、$$が先に解決されてPIDになってしまう。

$ a=foo
$ foo=1
$ echo $$a
1136a

正しくはこちら。

$ echo ${!a}

[[[test

シェルで分岐をする場合によく使うこれ。[testと同じコマンドなのでどちらを使ってもいい。

# if test $a -gt 1でも同じ

if [ $a -gt 1 ]
then
    echo $a greater than 1
else
    echo $a less than or equal to 1
fi

だけどこの2つはシェルとは別のコマンドなので、空白文字や空文字の場合に注意しないといけなくてめんどくさい。

$ a=""
$ [ $a = "" ] # unary operater expectedエラー
$ [ "$a" = "" ] # ok
$ a="1 2"
$ [ $a = "" ] # too many argumentsエラー
$ [ "$a" = "" ] # ok

bashを使うのであれば、今は常に[[を使うのが良いと思う。

$ a=""
$ [[ $a = "" ]]
$ a="1 2"
$ [[ $a = "" ]]

正規表現でマッチ

[[=~演算子を使うと正規表現が使える。

text='a b c'
if [[ $text =~ ^a.*$ ]]
then
    echo match
fi

ここでは、安易に正規表現をクオートしてはいけない。クオートすると、正規表現で特別な扱いされる文字が全てバックスラッシュでエスケープされる。シングルクオートとダブルクオートでどちらも同じ。

[[ $text =~ '^a.*$' ]] # \^a\.\*\$と同等

正規表現の途中でスペース等を含ませたい場合は、その部分だけクオートするとよい。

[[ $text =~ ^'a b'.*$ ]]

実際にどのようなエスケープがされるのかは、set -xしておくとエスケープされた後の正規表現を見ることができる。

if&&||

ifの代わりに、&&||が使える。エラー処理はこちらを使った方が読みやすい。

$ test -f file || echo 'file not exist' >&2 && exit 1

配列

簡単な使い方。

# 初期化
a=()

# 末尾に追加
a=("${a[@]}" test)

# 先頭に追加
a=(test "${a[@]}")

# 内容を列挙
echo "${a[@]}"

単純にecho $aすると、先頭の要素しか対象にならない。

配列に特定の要素が存在するか

専用の機能は無いが、[[コマンドの=~演算子が使える。

a=(cafe beef)
if [[ " ${a[@]} " =~ " cafe " ]]
then
    echo found
fi

=~正規表現でマッチさせる演算子なので、スペースで要素の区切りを表現してあげなければならない。

計算する

$((10 + 1))のように書くと外部コマンドなく計算ができる。

$ N=10
$ echo $((10 + $N))
20
$ echo $((1 * 3 - 1))
2

数字の前に基数を書ける。

$ echo $((16#0F + 8#10))
23

((expr))だけの場合は評価をするだけなので、条件式に使うと便利。

$ ((10 > 8#10)) && echo ok
ok

trap

trap は、SIGINT, SIGHUP などの他に以下の擬似シグナルが存在し、それらにハンドラを割り当てられる。

擬似シグナル 意味
ERR コマンド実行結果がエラーなら都度実行される
EXIT コマンドを終了したときに実行される
DEBUG コマンド実行のたびに都度実行される

一時ファイルを必ず削除したい場合はEXITに登録しておくと良い。利用可能なシグナルはtrap -lで表示できる。

変数展開

bashは通常のshと比べてとても多くの展開式が使える。

変数置換

古くからある展開式。

書き方 意味
${v:-text} vが未定義ならtextに置換
${v:=text} vが未定義ならtextをセットして置換
${v:?text} vが未定義ならtextを表示して終了
${v:+text} vが 定義されていたら textに置換

変数の少し変わった扱い方。

書き方 意味
${#v} 文字列の長さ、または配列の要素数に展開
${!v} vの値を変数名として展開

文字列置換

書き方 意味
${v%glob} vの末尾から最短マッチした部分を削除
${v%%glob} vの末尾から最長マッチした部分を削除
${v#glob} vの先頭から最短マッチした部分を削除
${v##glob} vの先頭から最長マッチした部分を削除
${v/s1/s2} vに含まれるs1を1つだけs2に置換
${v//s1/s2} vに含まれるs1を全てs2に置換

bash 4以降から利用可能な式

便利な式だけど、macOS High Sierraはまだ3系なので利用箇所には注意。

書き方 意味
${v^} 先頭の文字を大文字にして展開
${v^^} 全ての文字を大文字にして展開
${v,} 先頭の文字を小文字にして展開
${v,,} 全ての文字を小文字にして展開
${v~} 先頭文字の大文字小文字を反転
${v~~} 全ての文字で大文字小文字を反転

オプションを扱う

色々方法はあるけどgetoptsがいちばん面倒がなくて良いと思う。以下のコードでは、-fに引数がない・不明なオプションが与えられた時に、getoptsがエラーを出してくれる。

while getopts xf: OPT
do
    case $OPT in
    f)    filename="$OPTARG" ;;
    x)    flagx=1 ;;
    esac
done
shift $((OPTIND - 1))

だけど不明なオプションでもエラーとしたくない場合は少しめんどくさい。getopts最初の引数先頭に:をつけるとエラーにならない代わりに、特殊なオプション文字として:または?のどちらかが入っている。

文字 意味
? 不明なオプション文字が使われた
: 引数が必要なオプションだけど無かった

このように使う。

while getopts :xf: OPT
do
    case $OPT in
    :)  echo usage: $(basename $0) [-x] [-f file] >&2
        exit 1 ;;
    x)  flagx=1 ;;
    \?) x=$((OPTIND - 1))
        echo unknown option: ${!x} >&2
        exit 1 ;;
    esac
done
shift $((OPTIND - 1))

不明なオプションだった場合、具体的にどの文字が使われたのか調べる方法は特に用意されてなさそうだったので、上記のように自分で計算する。

先頭のアレ

シバン(shebang)というらしい。

#!/usr/bin/env bash

bashを使う場合、環境によってパスが違うかもしれないのでenvでラップするのをよく見かける。

Windows 10でBluetoothデバイスのPINコードが表示されない

Bluetoothバイスを追加するときに、PINコードを入力する必要があるけど、なぜか、本来は画面に表示されるべきPINコードがまったく表示されず、ペアリングができなくなる場合があった。

PINコード入力画面

バイス側のペアリング情報を削除しても変わらない。

この場合、設定アプリの「Bluetoothとその他デバイス」の画面下のほうにある「デバイスとプリンター」リンクを選び、コントロールパネル側の「デバイスとプリンター」画面を開く。次に、右クリックメニュー→デバイスとプリンターの追加を選んで作業を進める。途中でPINコードの入力が促されるので、テキストボックスの下にある「または、パスコードを接続先のデバイスで入力してください」リンクを開いて、そこから入力するといい。

この記事が参考になった。

nadegata.info

Go+goyaccでシェルを実装する

この記事はQiitaで公開されていました

この記事は、Go2 Advent Calendar 2017の17日目です。

特にこれといったネタがなかったので、goyaccを使ってシェルを実装してみました。

何をしたのか

Linux/macOSなどのUnix系OSには、bashzshfishなどあります。これらはとてもよく出来ていて、ユーザインタフェースとして使う分にはあまり困りません。しかし、プログラムを書く言語としては、少々貧弱であったり直感的でない文法があったりして、微妙だなと思っていました。例えばスペースを含む場合、どこでクオートが必要で、どこで書いてはいけないのかを正しく行うのは難しいです。

Plan 9にはrcという、とてもCに近い文法のシェルがあって、これはプログラミング言語としてよくできています。Infernoのシェルにも、モジュールをロードするなど面白い要素があります。これらを参考に、今年はGoでプログラムを書く言語としてのシェルを実装しました。

このシェルは、プロンプトやヒストリなど、インタラクティブな機能はほとんどありません。また、最低限動作する程度なので、必要だけれど実装が終わっていない機能などはいっぱいありますが、最低限は使える状態まで実装できたかなと思います。

使い方

以下のコマンドでインストールできます。

$ go get github.com/lufia/qsh

qshを実行しても、現在はプロンプトなど一切出ませんが、1行入力すれば入力されたコマンドを実行します。終了する場合は ctl+d を入力してください。

$ qsh

ある程度は他のシェルと同じですが、Plan 9rcInfernosh を参考にしているので、少し変わった文法があります。

文法

コメント

コメントは他のシェルと同じで#から行末までを無視します。ただし、文字の途中に含まれるものはそのまま文字として扱います。

# この行は無視する
echo a#b # "a#b"と出力
echo '#E'   # "#E"と出力

コマンド実行

普通に書けばコマンドとして実行します。他と異なり、コマンド名に/を含む場合でもPATHから探して実行します。

ls -l
gitlab/users -a  # $HOME/bin/gitlab/usersを実行

PATHにサブディレクトリが作れるのはPlan 9でも多用されていますが、とても便利だと思います。

変数

変数は全て一次元のリストです。リストは(a b c)のように、カッコとスペースで表します。a=1のように書くこともできますが、これはa=(1)と等価です。また、全て大文字の変数は、自動的に環境変数として昇格するためexportする必要はありません。

ranks=(S A B C)
echo $ranks     # "S A B C"と出力

API_TOKEN=xxxx   # 全て大文字なら環境変数になる
bash -c 'echo $API_TOKEN'  # "xxx"と出力

特別に、PATHという文字を含む環境変数は、他のプロセスが参照した時に困るため、エクスポートする時に要素の区切りをfilepath.ListSeparatorへ変更しています。

PATH=(/bin /usr/bin)
echo $PATH           # "/bin /usr/bin"と出力
bash -c 'echo $PATH' # "/bin:/usr/bin"と出力

変数を使って変数を間接参照することもできます。

Jan=1
January=Jan
c=January
echo $$$c    # "1"と出力

スペースもまともに扱えます。

touch 'a b'
args=(-l 'a b')
ls $args
# ls -l 'a b'を実行

今のところ、リストから特定の要素を取り出すことはできませんが、近いうちに$a(1)のような書き方で取り出せるようにする予定です。

If文・For文

これらは、よくあるシェルと見た目は異なります。Infernoのシェル由来ですが、ifの条件ブロックには複数のコマンドを書けるので少し便利です。

for i in 1 2 3 {
    echo $i
}

if { echo a | grep -q . } {
    echo match
}

&&||で繋げることもできます。こっちは他のシェルと同じです。

cmp -s a b && echo same  # aとbが同じ内容ならsameと出力
cmp -s a b || echo diff  # aとbが異なる内容ならdiffと出力

リダイレクト・パイプ

普通ですね。

echo hello >out   # 出力
echo hello >>out  # 追記
cat <in           # 入力
echo hello | wc   # パイプ

モジュール

少し実験的な機能も入れてみました。load命令で、Goで実装した関数を呼び出せます。Goのpluginパッケージを使って、シェルの機能を拡張するものです。

まずGoで以下のようなプラグインを実装します。

package main

var SampleModule = map[string]string{
    // Hello関数をhelloという名前でシェルから呼び出せるようにする
    "hello": "Hello",
}

func Hello(args []string) ([]string, error) {
    a := make([]string, 0, len(args)+1)
    a = append(a, "hello")
    return append(a, args...), nil
}

このとき、ファイル名はsnake_caseで付けてください。また、モジュールの名前(上記の場合はSampleModule)は、ファイル名をCamelCaseにして、末尾に Module を付けたものになります。上記例は、ファイル名がsample.goなので、モジュール名はSampleModuleです。sys_util.go の場合は SysUtilModule になります。

モジュールの実装ができたら、プラグインとしてコンパイルします。Go 1.9.2現在、Linuxしかサポートされていません

$ go build -buildmode=plugin sample.go

これで sample.so が生成できるので、シェルからロードして使いましょう。モジュールは${name}で呼び出します。

load sample          # プラグインの読み込み
echo ${hello world}  # "hello world"と出力

実装について

簡単にですが、実装について説明します。言語を実装する場合、ざっくり以下のフェーズを実装する必要があります。

他にも、最適化を行う場合もありますし、コード生成を行わずにツリーを直接実行する場合もありますが、単純な言語でない限りはコード生成を行ったほうが便利だと思います。

今回はgoyaccを使ったので、それを前提に書きます。goyacc自体の説明は、以下の記事を参考にしてください。

または、ANSI以前のC言語ですが、UNIXプログラミング環境にもyaccを使って計算機を作る章があり、とてもわかりやすいのでオススメです。

注意事項

全部書くと長くなるので、以下のコードは、雰囲気を知ってもらうために色々と省略して書いています。実際に動作するコードが見たい場合はリポジトリのコードを参照してください。

字句解析

字句解析は、言語のトークンを分割するものです。例えば以下の場合。

if { true } {
    echo a is $a | wc
}

これは次のように分割します。

type Node struct {
    Type int
    Str  string
}

Node{Type: IF}
Node{Type: '{'}
Node{Type: WORD, Str: "true"}
Node{Type: '}'}
Node{Type: '{'}
Node{Type: '\n'}
Node{Type: WORD, Str: "echo"}
Node{Type: WORD, Str: "a"}
Node{Type: WORD, Str: "is"}
Node{Type: '$'}
Node{Type: WORD, Str: "a"}
Node{Type: '|'}
Node{Type: WORD, Str: "wc"}
Node{Type: '\n'}
Node{Type: '}'}

ただし、goyaccを使う場合、字句解析のインターフェイスが決められていて、1回の呼び出しでは1つだけトークンを(lvalに詰めて)返すように実装しなければなりません。インターフェイスは以下の通りです。

type yyLexer interface {
    Lex(lval *yySymType) int
    Error(e string)
}

そのため、1文字読んで、区切りだったら戻しておいて次の呼び出しに使うことがよくあります。Goでは、text/scannerが用意されていて便利そうだったのですが、Scanner.Nextはまだ返すべき文字が残っていても入力を読むまで待ってしまうため、微妙に使いづらかったです。代わりに、io.RuneScannerを満たすbufio.Readerを使うといいでしょう。

初期のLex()は以下のような雰囲気です。yySymType型については構文解析のところで説明します。

const EOF = -1

type Lexer struct {
    f   io.RuneScanner
    buf bytes.Buffer
}

func (l *Lexer) Lex(lval *yySymType) int {
    l.buf.Reset()
    var c rune
    for {
        c, _, err := l.f.ReadRune()
        if err != nil {
            return EOF
        }
        if c != ' ' && c != '\t' {
            break
        }
    }
    switch c {
    case EOF:
        return -1
    case '$', '{', '}', '\n', '|':
        return int(c)
    case '\'':
        // 省略
    default:
        l.buf.WriteRune(c)
        for {
            c, _, err := l.f.ReadRune()
            if err != nil {
                break
            }
            if c == EOF || unicode.IsSpace(c) || c == '$' || c == '{' || ... {
                l.f.UnreadRune()
                break
            }
            l.buf.WriteRune(c)
        }
        lval.tree = &Node{Type: WORD, Str: l.buf.String()}
        return WORD
    }
}

あとは、必要に応じてプログラムを分割するようなコードを書けばいいです。この辺りのコードはqsh/lex.goで実装しました。

構文解析

次に、字句解析で分割したトークンを使って、言語のツリーを作ります。このフェーズは主にgoyaccで行います。YaccBNFに近い記法を使って、言語の文法を定義するものです。例えばシェルで1つのコマンドを表すコードは以下のようなものになります。

%term IF
%term WORD
%left IF
%left '|'
%%
prog:
    {
        return 1
    }
|   line '\n'
    {
        Compile($1)
        return 0
    }

line:
    cmd
|   cmd ';' line    { $$ = New(LIST, $1, $3) }

cmd:
|   IF block block  { $$ = New(IF, $2, $3) }
|   simple
|   cmd '|' cmd     { $$ = New(PIPE, $1, $3) }

block:
    '{' body '}'    { $$ = New(BLOCK, $2, nil) }

body:
    cmd
|   cmd ';' cmd     { $$ = New(LIST, $1, $3) }
|   cmd '\n' cmd    { $$ = New(LIST, $1, $3) }

simple:
    word
|   simple word     { $$ = New(LIST, $1, $2) }

word:
    '$' word        { $$ = New(VAR, $2, nil) }
|   WORD

これは、name: rule { code } | rule...のような書き方になっています。ruleにマッチした場合はcodeが実行されます。慣れるまでは読みづらいかもしれませんが、例えばsimpleの定義は

  1. wordが1回だけ登場する
  2. simpleに続いてwordが登場する

のどちらかである、と読みます。じゃあwordとは何なのかというと、

  1. Lex()$を返したトークンに続いて word が登場する
  2. Lex()WORDを返したトーク

のどちらか、という定義になっています。

ルールの読み方

例えばlsですが、まずはLex()が単語を読んでWORDを返します。そのため、wordの2番目のルールにマッチして、次からはwordとして扱われるようになります。wordsimpleの1番目のルールにもマッチします。そのため、順に遡っていって、最終的にはlineとして扱われます。'\n'があれば、プログラムとして満たしているのでパーサは終わります。

次に、ls $optなど2つ以上のトークンで構成される場合ですが、まずlssimpleとして扱われます。次の$optは、Lex()で分割したトークンとしては'$'WORDであり、これはwordの1番目のルールにマッチするのでwordです。そのため、simpleに続いてwordが登場するパターンとなり、simpleの2番目のルールにマッチしてsimpleです。

3つ以上続く場合も同じですね。

トーク

上のYaccコードで、$$とか$1のような書き方がありましたが、これは%unionで定義した型のメンバー変数が対応しています。

%union {
    tree *Node
}
%type<tree> line block body assign
%type<tree> cmd simple word
%type<tree> WORD

例えば以下の場合、

word:
    '$' word    { $$ = New(VAR, $2, nil) }
|   WORD        { $$ = $1 }

まずはWORDのルール。字句解析の章でLex()の引数にyySymTypeという型が使われていましたが、これはYaccのコードで%unionを使って定義した型そのものです。この記事で書いたLex()は、lval.treeにポインタを代入していました。また、$1などは%type<X>のルールによってメンバー変数に置き換えられるので、Lex()が代入したlval.treeの値が$1として参照できるようになっています。

$$は、別のルールからwordを参照した場合に、何を値とするかを設定するものです。$$が何も設定されなければ、$1の値が暗黙的に使われます。WORDのルールで$1(実際はLex()で設定した値)がそのまま$$になるため、'$' word {}の中で書かれた$2はそのままLex()の結果です。

構文解析の開始

Yaccで記述したパーサは、yyParse(l *yyLexer)関数を呼ぶと実行されます。yyParse()は内部的にl.Lex()を必要なだけ呼び出し、プログラムを満たせばyyParse()を抜けます。上記の場合はprogを満たした時点で(simpleに続けて'\n'が入力されたら)関数を抜けます。

一般的な言語の場合は、yyParse()を1回だけ呼び出せばよいこともありますが、シェルの場合はコマンド実行が終わっても次の入力を待つ必要があります。yyParse()は文法エラーなどがあった場合に非ゼロを返すので、エラーになるまで何度も繰り返すようにしました。

var l Lexer
f := bufio.NewReader(os.Stdin)
l.Init(f)
for yyParse(&l) == 0 {
}

Yaccreturnすると、その値がyyParse()の戻り値として返るため、文法は正しいけどエラーにしたい場合などは、この方法を使うこともできます。

prog:
    {
        return 1
    }
|   line '\n'
    {
        return 0
    }

最終的に、字句解析のところで書いた以下のコードは、

if { true } {
    echo a is $a | wc
}

yyParse()の結果このようなツリーになります。

type Node struct {
    Type  int
    Str   string
    Left  *Node
    Right *Node
}

f:id:lufiabb:20200328205019p:plain
ツリー図

こういったツリーが完成したら構文解析は終わりです。

コード生成

最後にコード生成です。今回はシェルなので、マシン語コンパイルする必要はありません。なので前の章で作成したツリーをそのまま実行してもいいのですが、個人的には、ループやユーザ定義関数などの実装を予定しているならコード生成を行ったほうが扱いやすいと思います。

今回実装したシェルでは、ls $optは以下のように内部コードへ変換します。

# ls $opt
MARK       # 新規スタックを作成する
PUSH ls    # lsという文字をスタックにpush
PUSH opt   # optという文字をスタックにpush
VAR        # 1つ取り出して変数参照; 結果をスタックに入れる
SIMPLE     # スタックに溜まっているリストを実行

if文の場合は少し複雑になります。以下はif { cmp -s a b } { echo ok }の場合です。

# if { cmp -s a b } {
#     echo ok
# }
MARK       # 新規スタックを作成する
PUSH cmp   # cmpという文字をスタックにpush
PUSH -s    # -sという文字をスタックにpush
PUSH a     # aという文字をスタックにpush
PUSH b     # bという文字をスタックにpush
SIMPLE     # スタックに溜まっているリストを実行
IF         # 実行結果が正常終了なら1つ飛ばす(GOTOをスキップ)
GOTO end   # endラベルへジャンプ
MARK       # 新規スタックを作成する
PUSH echo  # echoという文字をスタックにpush
PUSH ok    # okという文字をスタックにpush
SIMPLE     # スタックに溜まっているリストを実行
end:       # endラベル; ls aがエラーならここに飛ぶ

こういった内部コードは、構文解析で作成したツリーがあれば比較的簡単に実装できます。

type Cmd struct {
    pc int
}

type Code struct {
    steps []func(cmd *Cmd)
}

func (c *Code) emit(f func(cmd *Cmd)) {
    c.steps = append(c.steps, f)
}

func build(c *Code, p *Node) {
    if p == nil {
        return
    }
    switch p.Type {
    case WORD:
        c.emit(Push(p.Str))
    case SIMPLE:
        c.emit(Mark)
        build(c, p.Left)
        c.emit(Simple)
    case LIST:
        build(c, p.Left)
        build(c, p.Right)
    case BLOCK:
        build(c, p.Left)
    case VAR:
        c.emit(Mark)
        build(c, p.Left)
        c.emit(Var)
    case IF:
        build(c, p.Left)
        c.emit(If)
        var end Label
        c.emit(Goto(&end))
        build(c, p.Right)
        end.pos = len(c.steps)
    }
}

以上で完成です。あとはコード生成が終わったCode.stepsを順番に実行していけば、それらしい動きをすると思います。

最初にも書きましたが、動作する完全なコードはリポジトリのコードを参照してください。

おわりに

今回、12月の頭からシェルを実装しはじめて、だいたい動作するかなというところまでで50コミット2,000行くらいでした。スペースの扱いとか、変数の間接参照とか、PATH以下にサブディレクトリを作れるとか、色々と好みの実装ができたかなという気持ちです。まだ足りない部分は一杯あるので、継続して開発していきます。

TimeoutHandlerでnet/httpのリクエストをタイムアウトさせる

この記事はQiitaで公開されていました

素直な実装

特に何もしない場合、net/httpタイムアウトしません。以下のサーバにHTTPアクセスをすると、1分後にhello!というレスポンスを返します。

package main

import (
    "log"
    "net/http"
    "time"
)

type handler struct{}

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    <-time.After(1 * time.Minute)
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("hello!")
    log.Println("ok")
}

func main() {
    log.SetFlags(0)
    log.SetPrefix("server: ")

    var h handler
    http.Handle("/", &h)
    http.ListenAndServe(":8080", nil)
}

以下は手元での実行例です。

$ go run server.go &
$ curl http://localhost:8080/
server: ok
hello!

ハンドラのタイムアウトを設定する

どのリクエストも一律同じ時間でタイムアウトさせれば良い場合、http.TimeoutHandler()を使えば、一定時間でクライアントに503 Service Unavailableを返すようになります。

タイムアウトが発生したかどうかは、http.ResponseWriterWrite()http.ErrHandlerTimeoutを返すかどうかで判断できます。

package main

import (
    "log"
    "net/http"
    "time"
)

type handler struct{}

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    <-time.After(1 * time.Minute)
    w.WriteHeader(http.StatusOK)
    _, err := w.Write([]byte("hello!"))
    switch err {
    case http.ErrHandlerTimeout:
        log.Println("timeout")
    case nil:
        log.Println("ok")
    default:
        log.Println("err:", err)
    }
}

func main() {
    log.SetFlags(0)
    log.SetPrefix("server: ")
    var h handler
    http.Handle("/", http.TimeoutHandler(&h, 10*time.Second, "timeout!"))
    http.ListenAndServe(":8080", nil)
}

このサーバへリクエストを送ると、10秒後にタイムアウトして、エラーとしてクライアントへ返却されます。ただしhandler.ServeHTTP()は実行され続けているので、1分後に本来の処理が完了します。

$ go run server.go &
$ curl http://localhost:8080/
timeout!        # 10秒後
server: timeout # 1分後

このため、アプリケーションによっては、タイムアウトしていたらロールバックする等の処理が必要かもしれません。

その他のタイムアウト

http.TimeoutHandler()の他にも、net/httpには色々なタイムアウトがあります。Then complete guide to Go net/http timeoutsには、どのタイムアウトがどこにかかるのかなど詳しく書かれているので、がとても分かりやすくおすすめです。

Goでサブディレクトリを含むProtocol Buffers定義ファイルを扱う場合の上手な書き方

この記事はQiitaで公開されていました

問題

Goは、$GOPATH/src/ 以下にパッケージのソースコードを置くルール。自分で書いたコードも例外ではないので、以下の例では、自分で書くコードを $GOPATH/src/example.com/app に置いている。

$GOPATH/
└ ─ /src
    ├ ─ /example.com
    │   └ ─ /corp
    │       └ ─ /app
    │           └ ─ /rpc
    │               ├ ─ /user
    │               ├ ─ /blob
    │               └ ─ /log
    └ ─ /example.org
        └ ─ /app
            └ ─ /rpc
                └ ─ /main.go

Goでは、型や変数をpackagename.TypeNameのように書くので、UserMessageという型を、パッケージを分けてuser.Messageとする場合がある。だけども上の通り、別パッケージにするにはディレクトリを分ける必要があるので、Protocol Buffersコンパイラ(protoc)で生成するコードも例外なく、パッケージを分けるなら別の階層に置かなければならない。

このような、 Protocol Buffers(.proto)ファイルがサブディレクトリの.protoファイルを参照する 場合、出力先やオプションを適切に使わないと手間がかかったりビルドできなかったりする。

この記事で想定するProtocol Buffers定義の階層

$GOPATH/src/example.com/app/rpc 以下に .proto ファイルを全て置いた状態を想定する。

$GOPATH/src/example.com/
└ ─ /app
    └ ─ /rpc
        ├ ─ /user
        │   └ ─ /user.proto
        ├ ─ /blob
        │   └ ─ /blob.proto
        ├ ─ /log
        │   └ ─ /log.proto
        └ ─ /main.proto

最終的にどうすればいいか

protoファイルの書き方

参照されるだけの .proto ファイルは、option go_packageで正しいimport pathを設定しておく。以下は user.proto ファイルの例。

package user;
option go_package = "example.com/app/rpc/user";

参照する側の .proto ファイルは、上記に加えて、importの書き方をファイル階層に合わせる。

package rpc;
option go_package = "example.com/app/rpc";

import "user/user.proto";

protoc-gen-goのオプション

Goのコードを生成する場合は以下のコマンドで行う。

$ protoc -Irpc --go_out=plugins=grpc:$GOPATH/src rpc/main.proto
$ protoc -Irpc --go_out=plugins=grpc:$GOPATH/src rpc/user/user.proto
  • -Iオプションで.protoファイルのimportで参照するルートディレクトリを指定する
  • --go_out=オプションで出力先を変更する

Ruby等は、そのまま実行すれば良さそう。

$ plugin="protoc-gen-grpc=$(which grpc_tools_ruby_protoc_plugin)"
$ protoc -Irpc --ruby_out=. --grpc_out=. --plugin=$plugin rpc/main.proto
$ protoc -Irpc --ruby_out=. --grpc_out=. --plugin=$plugin rpc/user/user.proto

この書き方で使い回しができる .proto になる。

色々な失敗案

参考のため、試行錯誤した結果と使えない理由をまとめた。

デフォルトの動作

ディレクトリのファイルを参照する .proto ファイルは、特にオプションを入れない場合、生成された.pb.goファイルのimportパスが $GOPATH/src/ からになっていない。例えば main.proto ファイルで

syntax = "proto3";

package rpc;
import "user/user.proto";

と書くと、生成されたGoのコードは

package rpc

import "user"

のようなimportになってしまってパッケージを読み込めない。

go_packageオプション

この問題に対応するため、.proto ファイルのオプションでoption go_packageがあるけど、これを書いてしまうと、Goソースコードの生成先に、さらにパッケージ階層が作られてしまう。

参照される側:

syntax = "proto3";

package user;
option go_package = "example.com/app/rpc/user";

参照する側:

syntax = "proto3";

package rpc;
option go_package = "example.com/app/rpc";
import "user/user.proto";

生成コマンド:

$ protoc -Irpc --go_out=plugins=grpc:. rpc/main.proto
$ protoc -Irpc --go_out=plugins=grpc:. rpc/user/user.proto

結果:

$GOPATH/
└ ─ /src
    └ ─ /example.com
        └ ─ /app
            ├ ─ /example.com      <- 余計な階層が作られる
            │   └ ─ /app
            │       └ ─ /rpc
            │           ├ ─ /user
            │           │   └ ─ /user.pb.go
            │           └ ─ /main.pb.go
            └ ─ /rpc
                ├ ─ /user
                │   └ ─ /user.proto
                ├ ─ /blob
                │   └ ─ /blob.proto
                ├ ─ /log
                │   └ ─ /log.proto
                └ ─ /main.proto

protoc-gen-goのオプションを試す

protoc--go_out=オプション経由で、カンマ区切り文字列を指定すれば、protoc-gen-goへオプションを与えることができるがどれも微妙。

import_prefix

このオプションは、指定した文字列が標準パッケージ以外全ての先頭に付く。protocで生成したパッケージだけに付くなら問題ないけれど、github.com/golang/protobuf/protoの頭にもついてしまうのでよくない。

import_path

これは .proto ファイルにoption go_packageがない場合のみ、コマンドラインから与えることができるオプション。なので動作としてはoption go_packageと何も違いがない。

vendorに入れてみる

--go_out=で出力先ディレクトリが決められるので、option go_packageで階層が掘られてしまうならvendor以下に入れてしまおう案。

基本的にはうまく動いたけれど、depで管理していないパッケージがvendor以下に入ってしまうため、dep initdep ensureがエラーになってしまう。現状では無視することもできなさそうなので、使えない。