#author("2020-02-15T03:52:48+09:00","default:haikikyou","haikikyou")
#author("2020-02-15T03:53:20+09:00","default:haikikyou","haikikyou")
[[moritetuのIT関連技術メモ]]

#contents


* shUnit2 [#o3f32c01]

xUnit系テストツールのシェル版である。~

* インストール [#wecdc957]

特別な操作は不要。~
ソースをダウンロードして、任意の場所に設置するだけである。~
非常に簡単である。


* テスト [#z34b7c96]

特別な設定は不要であり、典型的な実行方法はテストプログラムの中でshunit2プログラムをインクルードするだけである。

&label(sample){例};'' sample.sh''

#geshi(bash){{{
#! /bin/sh

testEquality() {
  assertEquals 1 1
}
# shunit2テストをインクルード、テストが実行される
. /path/to/shunit2
}}}


** テストプログラム [#effb8d19]

- テストファイルは、suffixが&code(){_test.sh};である。
- テスト対象の関数は、&code(){test};を接頭辞に持つものである。~
#geshi(bash){{{
# テスト対象
testVarShouldNotbeEmpty() {
    assertNotNull '$var is not empty' "$msg"
}
# テスト対象でない
hoge() {
    assertNotNull '$var is not empty' "$msg"
}
}}}
- テストは、ファイル内の上から定義されている順に実行される。~



&label(info){補足};
- テスト関数は&code(){eval};で評価される。
- &code(){egrep};で以下の条件にマッチする関数名がテスト対象である。
#geshi{{{
^\s*((function test[A-Za-z0-9_-]*)|(test[A-Za-z0-9_-]* *\(\)))
}}}
** テストの実行 [#v1a0f600]

*** shunit2から実行 [#cb7b2e5b]

テストは、テストプログラム内からshunit2をロードする、または、shunit2にテスト対象ファイル名を渡すことで実行される。

#geshi(bash){{{
$ shunit2 <a test file> [<func> [<func>...]]
}}}

''sample_test.sh''

#geshi(bash){{{
#! /bin/sh

testEquality() {
    assertEquals 1 1
}

testTrue() {
    assertTrue "[ 1 -eq 1 ]"
}
}}}

''実行''

ファイルのテスト関数をすべて実行する。

#geshi(bash){{{
$ shunit2 sample_test.sh
testEquality
testTrue

Ran 2 tests.

OK
}}}

ファイルの特定のテスト関数を実行する。

#geshi(bash){{{
$ shunit2 sample_test.sh testTrue
testTrue

Ran 1 test.

OK
}}}


*** shunit2をインクルードして実行 [#d8c8ee6e]

''hoge_test.sh''
#geshi(bash){{{
# hoge_test.sh
testFunc() {
  :
}

. /path/to/shunit2
# この場合、$0がテスト対象ファイルと見なされる(つまり、hoge_test.sh)
}}}

''実行''

#geshi(bash){{{
$ bash hoge_test.sh
}}}


** テストスイートの実行 [#q74c6d92]
*** test_runner [#i0625066]

テスト実行のためのヘルパーである。~
suffixが、&code(){_test.sh};であるテストファイル見つけてまとめて実行してくれる(テストスイートの実行である)。~
特定のシェル環境で実行したり、指定がしなければデフォルトで複数のシェル環境でテストを実行してくれる。

#geshi(bash){{{
$ ./test_runner -h
usage: test_runner [-e key=val ...] [-s shell(s)] [-t test(s)]
}}}

デフォルトのシェル環境は以下のとおり。

#geshi{{{
/bin/sh ash /bin/bash /bin/dash /bin/ksh /bin/pdksh /bin/zsh
}}}

&label(sample){サンプル}; ''test_runnerの実行例''

shunit2の配下にあるテストでない場合は、shunit2の&code(){lib};ディレクトリの場所を指定すると流れる。~
テスト対象は、テスト実行ディレクトリ(&code(){$PWD};)にある&code(){_test.sh};のファイルである。

#geshi(bash){{{
$ tree ../shunit2
../shunit2
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── doc
│   ├── CHANGES-2.1.md
│   ├── RELEASE_NOTES-2.1.0.txt
│   ├── RELEASE_NOTES-2.1.1.txt
│   ├── RELEASE_NOTES-2.1.2.txt
│   ├── RELEASE_NOTES-2.1.3.txt
│   ├── RELEASE_NOTES-2.1.4.txt
│   ├── RELEASE_NOTES-2.1.5.txt
│   ├── RELEASE_NOTES-2.1.6.txt
│   ├── RELEASE_NOTES-2.1.7.md
│   ├── TODO.txt
│   ├── contributors.md
│   └── design_doc.txt
├── examples
│   ├── equality_test.sh
│   ├── lineno_test.sh
│   ├── math.inc
│   ├── math_test.sh
│   ├── mkdir_test.sh
│   ├── mock_file.sh
│   ├── mock_file_test.sh
│   ├── party_test.sh
│   └── suite_test.sh
├── lib
│   ├── shflags
│   └── versions
├── shunit2
├── shunit2_args_test.sh
├── shunit2_asserts_test.sh
├── shunit2_failures_test.sh
├── shunit2_macros_test.sh
├── shunit2_misc_test.sh
├── shunit2_standalone_test.sh
├── shunit2_test_helpers
└── test_runner

3 directories, 35 files
$ tree
.
└── hoge_test.sh

0 directories, 1 file
$ LIB_DIR=../shunit2/lib ../shunit2/test_runner
#------------------------------------------------------------------------------
# System data.
#

$ uname -mprsv
Linux 3.10.0-1062.9.1.el7.x86_64 #1 SMP Fri Dec 6 15:49:49 UTC 2019 x86_64 x86_64

OS Name: Linux
OS Version: CentOS Linux 7 (Core)

### Test run info.
shells: /bin/sh ash /bin/bash /bin/dash /bin/ksh /bin/pdksh /bin/zsh
tests: hoge_test.sh


#------------------------------------------------------------------------------
# Running the test suite with /bin/sh.
#
shell name: sh
shell version: GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu)

--- Executing the 'hoge' test suite. ---
testEquality

Ran 1 test.

OK


#------------------------------------------------------------------------------
# Running the test suite with ash.
#
runner:WARN unable to run tests with the ash shell


#------------------------------------------------------------------------------
# Running the test suite with /bin/bash.
#
shell name: bash
shell version: GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu)

--- Executing the 'hoge' test suite. ---
testEquality

Ran 1 test.

OK


#------------------------------------------------------------------------------
# Running the test suite with /bin/dash.
#
runner:WARN unable to run tests with the dash shell


#------------------------------------------------------------------------------
# Running the test suite with /bin/ksh.
#
runner:WARN unable to run tests with the ksh shell


#------------------------------------------------------------------------------
# Running the test suite with /bin/pdksh.
#
runner:WARN unable to run tests with the pdksh shell


#------------------------------------------------------------------------------
# Running the test suite with /bin/zsh.
#
runner:WARN unable to run tests with the zsh shell
}}}
~

&label(study){実験};''テストプログラムがshunit2と別のディレクトリにある場合''

&code(){test_runner};のラッパーを作成して実行してみる(&code(){run.sh};)。~


''ディレクトリ構成''

ディレクトリ構成は以下のとおり。~
テストは、shunit2とは別のディレクトリにあるとする。

#geshi{{{
/home/guest/shunit2
 |- shunit2
 |- lib
 |- test_runner
 ...

/home/guest/mytests
  |- my_test.sh
  |- run.sh
}}}

''/home/guest/mytests/run.sh''

test_runnerを実行するためだけのラッパー。

#geshi(bash){{{
#!/usr/bin/env bash

export SHUNIT2_ROOT=${SHUNIT2_ROOT:-/home/guest/shunit2}
export LIB_DIR="$SHUNIT2_ROOT/lib"
export SHUNIT_INC="$SHUNIT2_ROOT/shunit2"

"$SHUNIT2_ROOT"/test_runner "$@"
}}}


''/home/guest/mytests/my_test.sh''

テストプログラムは、shunit2を環境変数で変えられるように作成しておく。

#geshi(bash){{{
#! /bin/sh

. "$SHUNIT2_ROOT"/shunit2_test_helpers

testEquality() {
  assertEquals 1 1
  assertEquals "$HOGE" "foo"
}

. "${TH_SHUNIT}"
}}}


以下では、シェルは&code(){/bin/sh};、テスト対象は&code(){my_test.sh};、環境変数として&code(){HOGE=foo};を定義して実行している。

#geshi(bash){{{
$ cd /home/guest/mytests
$ bash run.sh -s /bin/sh -t my_test.sh -e HOGE=foo

#------------------------------------------------------------------------------
# System data.
#

$ uname -mprsv
Linux 3.10.0-1062.9.1.el7.x86_64 #1 SMP Fri Dec 6 15:49:49 UTC 2019 x86_64 x86_64

OS Name: Linux
OS Version: CentOS Linux 7 (Core)

### Test run info.
shells: /bin/sh
tests: my_test.sh
HOGE=foo


#------------------------------------------------------------------------------
# Running the test suite with /bin/sh.
#
shell name: sh
shell version: GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu)

--- Executing the 'my' test suite. ---
testEquality

Ran 1 test.

OK
}}}

&label(sample){サンプル}; &ref(./test_runner_sample.zip);
*** suite(独自にテスト関数を指定して実行) [#z3dc9644]

&code(){suite};を用いると&code(){suite_addTest};で任意の関数をテスト対象に指定することができる。~
しかし、ソースコメントを見る限り2.1.0の時点で&code(){deprecated};となっている。

&label(sample){例}; ''suiteで独自のテスト関数を実行する''

''suite_test.sh''

#geshi(bash){{{
#! /bin/sh

my_test1() {
    assertEquals 1 1
}

my_test2() {
    assertTrue "[ 1 -eq 1 ]"
}

suite() {
    suite_addTest "my_test1"
    suite_addTest "my_test2"
}

. ../shunit2/shunit2
}}}

実行。

#geshi(bash){{{
$ bash suite_test.sh
my_test1
my_test2

Ran 2 tests.

OK
}}}

&label(fatal){注意};

&code(){suite_addTest};が設定されると、デフォルトの "test" を接頭辞にもつ関数の自動的なテストは行われないことに注意。~
これは、テスト対象が存在しない場合にのみ&code(){test};を接頭辞にもつテスト関数を探索するようになっているためである。

&label(warn){参考};
- https://github.com/kward/shunit2/blob/6d17127dc12f78bf2abbcb13f72e7eeb13f66c46/shunit2#L1304
* 特徴・機能 [#n2c66b8f]

詳細はドキュメントを参照のこと。~
動作で留意しておくべき点のみピックアップする。

** oneTimeSetUp、oneTimeTearDown [#n7e3d400]
- テストの失敗にかかわらず&code(){oneTimeTearDown};は実行される。
- テスト実行前、実行後に一度のみ実行される。
- &color(red){oneTimeSetUp関数がエラーの場合、テストは停止する。};


&label(sample){例}; ''oneTimeSetUpで失敗した場合''

#geshi(bash){{{
oneTimeSetUp() {
    echo "oneTimeSetUp"
    return 1
}

oneTimeTearDown() {
    echo "oneTimeTearDown"
}

setUp() {
    echo "setUp"
}

tearDown() {
    echo "tearDown"
}

testMy() {
    echo "testMy"
    assertEquals 1 1
}

testMy2() {
    echo "testMy2"
    assertEquals 1 1
}

. ../shunit2/shunit2
}}}

実行結果は以下のとおり。

#geshi(bash){{{
$ bash study_test.sh
oneTimeSetUp
shunit2:FATAL oneTimeSetUp() returned non-zero return code.
tearDown
oneTimeTearDown
ASSERT:unknown failure encountered running a test

Ran 0 tests.

FAILED (failures=1)
}}}

&label(sample){例}; ''oneTimeTearDown で失敗した場合''

当然ながらテストは実行された後のcleanupで失敗となる。

#geshi(bash){{{
oneTimeSetUp() {
    echo "=> oneTimeSetUp"
}

oneTimeTearDown() {
    echo "=> oneTimeTearDown"
    return 1
}

setUp() {
    echo "=> setUp"
}

tearDown() {
    echo "=> tearDown"
}

testMy() {
    echo "=> testMy"
    assertEquals 1 1
}

testMy2() {
    echo "=> testMy2"
    assertEquals 1 1
}

. ../shunit2/shunit2
}}}

実行結果は以下のとおり。

#geshi(bash){{{
$ bash study4_test.sh
=> oneTimeSetUp
=> setUp
testMy
=> testMy
=> tearDown
=> setUp
testMy2
=> testMy2
=> tearDown
=> oneTimeTearDown
shunit2:FATAL oneTimeTearDown() returned non-zero return code.
=> tearDown
=> oneTimeTearDown
shunit2:WARN oneTimeTearDown() returned non-zero return code.
ASSERT:unknown failure encountered running a test

Ran 2 tests.

FAILED (failures=1)
}}}
** setUp、tearDown [#n40e031b]

- テストごとに、テスト実行前、実行後に一度のみ実行される。~
- テストの失敗にかかわらず&code(){tearDown};は実行される。
- &code(){setUp};、&code(){tearDown};が失敗した場合は、テストは終了する。~
&code(){tearDown};はテスト終了時に再度呼ばれる。その後、&code(){oneTimeTearDown};が呼ばれる。~
&color(red){Ran 2 testsのようにレポートされるが、&code(){testXXX};は実行前であることに注意。};

&label(sample){例}; ''setUp関数がエラーの場合''

#geshi(bash){{{
oneTimeSetUp() {
    echo "oneTimeSetUp"
}

oneTimeTearDown() {
    echo "oneTimeTearDown"
}

setUp() {
    echo "setUp"
    return 1
}

tearDown() {
    echo "tearDown"
}

testMy() {
    echo "testMy"
    assertEquals 1 1
}

testMy2() {
    echo "testMy2"
    assertEquals 1 1
}

. ../shunit2/shunit2
}}}

実行結果は以下のとおり。

#geshi(bash){{{
$ bash study2_test.sh
oneTimeSetUp
setUp
shunit2:FATAL setup() returned non-zero return code.
tearDown
oneTimeTearDown
ASSERT:unknown failure encountered running a test

Ran 2 tests.

FAILED (failures=1)
}}}

&label(sample){例}; ''tearDown関数がエラーの場合''

なお、以下ではtestMyは失敗するようにしている。

#geshi(bash){{{
oneTimeSetUp() {
    echo "oneTimeSetUp"
}

oneTimeTearDown() {
    echo "oneTimeTearDown"
}

setUp() {
    echo "setUp"
}

tearDown() {
    echo "tearDown"
    return 1
}

testMy() {
    echo "Run testMy"
    assertEquals 1 2
}

testMy2() {
    echo "Run testMy2"
    assertEquals 1 1
}

. ../shunit2/shunit2
}}}

実行結果は以下のとおり。

#geshi(bash){{{
$ bash study3_test.sh
oneTimeSetUp
setUp
testMy
Run testMy
ASSERT:expected:<1> but was:<2>
shunit2:ERROR testMy() returned non-zero return code.
tearDown
shunit2:FATAL tearDown() returned non-zero return code.
tearDown
shunit2:WARN tearDown() returned non-zero return code.
oneTimeTearDown
ASSERT:unknown failure encountered running a test

Ran 2 tests.

FAILED (failures=3)
}}}
** Skipping [#t63c76f8]

assertのfailの実行と記録を行わない。~
startSkippingとendSkippingでスキップ対象範囲を囲む。~

&label(memo){メモ};~
これらの関数は内部的にスキップフラグをON/OFFにしているだけあり、assertやfail系関数の実行時にこのフラグが参照され、assertやfailを実行するか否かを判断している。

&label(sample){例}; ''スキップの例''

#geshi(bash){{{
testSkip() {
#    startSkipping
    echo "This message will be printed"
    assertEquals 1 1
#    endSkipping
    assertEquals 1 1
}


. ../shunit2/shunit2
}}}

実行結果は以下のとおり。

#geshi(bash){{{
$ bash skip_test.sh
testSkip
This message will be printed

Ran 1 test.

OK
}}}

Skipを有効にすると以下のとおり。

#geshi(bash){{{
$ bash skip_test.sh
testSkip
This message will be printed

Ran 1 test.

OK (skipped=1)
}}}
* レポート [#r54b7cf3]

レポートで出力される内容は、以下のとおりである。~
数値が何を意味しているのかは少し注意が必要(failures)である。

|~表示|~意味|h
|Ran n tests|nは、テスト対象となったテスト関数の数。テスト開始時に決まる。|
|skipped=|assertやfailがスキップされた数|
|failures=|失敗した実行数(assert、fail、テスト)|

&code(){Ran n tests};のnは、テスト対象関数として定義された時点でのテスト数である。~
実行されていなくとも、&code(){setUp};関数で失敗しても&code(){Ran n tests};と表示されるのはそのためである。

&label(study){実験}; ''テスト実行前にテスト終了させる場合のレポート''

通常はこのような事をしないので、あくまで実験である。~

#geshi(bash){{{
my_test1() {
    assertEquals 1 1
}

my_test2() {
    assertTrue "[ 1 -eq 1 ]"
}

suite() {
    suite_addTest "my_test1"
    suite_addTest "my_test2"
    exit # ここで停止してもRan 2 testsとなるはず
}

. ../shunit2/shunit2
}}}

実行すると以下のように &code(){Ran 2 tests};となる。

#geshi(bash){{{
bash suite_test.sh
ASSERT:unknown failure encountered running a test

Ran 2 tests.

FAILED (failures=1)
}}}

&label(memo){メモ}; ''テストの実行プロセス''

テストの実行プロセスは大まかに以下のとおり。

+ テスト対象ファイルの認識
+ oneTimeSetUpの実行
+ suiteがあればsuiteを実行。もしくは、<file> 関数名...が指定されて実行された場合は、shunit2が面倒を見てくれる。~
(suiteは実行されない)
+ 登録済みのテスト対象関数がなければ、shunit2がtestの接頭辞のテスト対象関数を抽出
+ テストスイートの実行
+ oneTimeTearDownの実行
+ テストレポートの出力

~
&label(info){補足}; ''詳細なテストレポート''

assertの数など少し詳細な情報が欲しいかもしれない。~
内部的には統計情報を持っているが、レポート生成関数では参照されていないようであった。

以下のようにすれば内部の情報も見ることができた。~
(他に正当なやり方があるかもしれないが調べる限り見つからなかった)

#geshi(bash){{{
test_test1() {
    startSkipping
    fail "This is not counted"
    assertEquals 1 1
    endSkipping
}

test_test2() {
    assertTrue "[ 1 -eq 1 ]"
    fail "hoge"
    assertTrue "[ 1 -eq 2 ]"
}


_DETAIL_REPORTED=0

oneTimeTearDown() {
    if [ $_DETAIL_REPORTED -ne 1 ]; then
        echo "=== Detail Report ==="
        echo "testsTotal=${__shunit_testsTotal}"    # Run n tests の n
        echo "testsPassed=${__shunit_testsPassed}"
        echo "testsFailed=${__shunit_testsFailed}"

        echo "assertsTotal=${__shunit_assertsTotal}"
        echo "assertsPassed=${__shunit_assertsPassed}"
        echo "assertsFailed=${__shunit_assertsFailed}"    # failures=の値
        echo "assertsSkipped=${__shunit_assertsSkipped}"  # skipped=の値
        echo "======"
        _DETAIL_REPORTED=1
    fi
}

. ../shunit2/shunit2
}}}

実行結果は以下のとおり。

#geshi(bash){{{
$ bash suite2_test.sh
test_test1
test_test2
ASSERT:hoge
ASSERT:
shunit2:ERROR test_test2() returned non-zero return code.
=== Detail Report ===
testsTotal=2
testsPassed=1
testsFailed=1
assertsTotal=6
assertsPassed=1
assertsFailed=3
assertsSkipped=2
======

Ran 2 tests.

FAILED (failures=3,skipped=2)
}}}

各表示の意味は以下のとおり。

|~表示|~意味|h
|testsTotal|テスト数、suite_addTestされたテスト関数の数|
|testsPassed|テストにパスした数、テスト関数単位|
|testsFailed|テストに失敗した数、テスト関数単位|
|assertsTotal|skipを含む、assert、failの数|
|assertsPassed|assertにパスした数|
|assertsFailed|assert失敗、fail、テストに失敗した数|
|assertsSkipped|スキップされたassert、failの数|

&code(){assertsFailed};の数が見た目とずれている気がするかもしれない。~
これは、assertやfailの失敗以外もfailedとしてカウントされているためである。~

https://github.com/kward/shunit2/issues/117

#geshi(bash){{{
test_test2() {
    assertTrue "[ 1 -eq 1 ]"
    fail "hoge"
    assertTrue "[ 1 -eq 2 ]"
    # 上記のassertで失敗するため、テスト関数もエラー終了となり、assertFailedもインクリメントされる。
}
}}}

以下のようにすれば期待する結果となる。

#geshi(bash){{{
test_test2() {
    assertTrue "[ 1 -eq 1 ]"
    fail "hoge"
    assertTrue "[ 1 -eq 2 ]"
    :
    # return 0 でもOK
}
}}}

~
&label(study){実験}; ''suiteの途中で不意に終了した例''

もう1つassertFailedに関して実験してみる。~
assertFailedがfailuresに含まれる例である。

#geshi(bash){{{
#! /bin/sh

my_test1() {
    assertEquals 1 1
}

suite() {
    suite_addTest "my_test1"
    exit
}

_DETAIL_REPORTED=0

oneTimeTearDown() {
    if [ $_DETAIL_REPORTED -ne 1 ]; then
        echo "=== Detail Report ==="
        echo "testsTotal=${__shunit_testsTotal}"
        echo "testsPassed=${__shunit_testsPassed}"
        echo "testsFailed=${__shunit_testsFailed}"

        echo "assertsTotal=${__shunit_assertsTotal}"
        echo "assertsPassed=${__shunit_assertsPassed}"
        echo "assertsFailed=${__shunit_assertsFailed}"
        echo "assertsSkipped=${__shunit_assertsSkipped}"
        echo "======"
        _DETAIL_REPORTED=1
    fi
}
. ../shunit2/shunit2
}}}

残念ながらこの結果からは、&code(){__shunit_assertsFailed};が&code(){1};にはならない。~
&code(){oneTimeTearDown};が実行される段階では、&code(){__shunit_assertsFailed};がインクリメントされていないからである。

#geshi(bash){{{
$ bash suite4_test.sh
=== Detail Report ===
testsTotal=1
testsPassed=0
testsFailed=0
assertsTotal=0
assertsPassed=0
assertsFailed=0
assertsSkipped=0
======
ASSERT:unknown failure encountered running a test

Ran 1 test.

FAILED (failures=1)
}}}
* 実行コンテキスト [#p37f6743]

shUnit2のテスト実行コンテキストは、テストファイル内の各テストで共通である。~
つまり、各テストはサンドボックス内で実行されていないため、他のテストに影響しうるということである。

例を見てみよう。~
極端な例ではあるが、イメージが掴めると思う。

&label(study){実験}; ''shunit2のテスト実行コンテキスト''

''~/bin/ls''

#geshi(bash){{{
$ cat ~/bin/ls
#!/usr/bin/env bash
echo "myls"
}}}

''sandbox_test.sh''

#geshi(bash){{{
test_no1() {
    export PATH="~/bin":"$PATH"
    result=$(ls)
    assertEquals "$result" "myls"
}

test_no2() {
    touch "test.txt"
    result=$(ls "test.txt")
    assertEquals "$result" "test.txt"
}

source ../shunit2/shunit2
}}}

実行すると以下のようになる。

#geshi(bash){{{
$ bash sandbox_test.sh
test_no1
test_no2
ASSERT:expected:<myls> but was:<test.txt>
shunit2:ERROR test_no2() returned non-zero return code.

Ran 2 tests.

FAILED (failures=2)
}}}

2番目のテスト&code(){test_no2};で失敗していることが分かる。~
これは、&code(){test_no1};で実行した&code(){export PATH};が&code(){test_no2};でも生きているということである。~
テスト実行のコンテキストで、&code(){eval};を使ってテスト関数を実行しているためである。~
あるテストで行なった他に影響し得る変更は、テストの最後でリセットするようにすればよい。

例えば、Bashで書かれたテストフレームワークであるbatsでは、各テストがサンドボックス内(サブシェル)で実行されるため、あるテストで定義した変数や環境変数等の定義は、他のテストには影響しない。

* 参考リンク [#q5e03c7e]

- https://github.com/kward/shunit2
- https://sites.google.com/site/paclearner/shunit2-documentation
- https://postd.cc/bash%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B/
- [[Bashアプリケーションをテストする>https://postd.cc/bash%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B/]] - &size(11){&color(gray){on https://postd.cc/};};

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
目次
ダブルクリックで閉じるTOP | 閉じる
GO TO TOP