VC PlusPlus:MSBuild バッチ処理変数

提供:yonewiki

VC PlusPlus:MSBuildに戻る。

概要

 バッチ処理は%(Batch)のように記述する変数です。


バッチ処理の定義の基礎

 例えば、以下のように書いた<ItemGroup>~</ItemGroup>があったとしたら


  <ItemGroup>
    <Batch Include="1" Element="en-US" />
    <Batch Include="2" Element="ja-JP" />
  </ItemGroup>


 %(Element)というバッチ処理がアイテムグループBatchの中に設定されたことになり、Batchの中のバッチとして扱う範囲の呼び出しができます。Includeに示した値はファイルなので、プロジェクトで読み込むには、vcxprojファイルのあるフォルダと同じ階層に1や2という名前のファイルが必要になります。Projectタグ直下に置くItemGroupの中のタグには最初の定義で要素IncludeかUpdateかRemoveが必須です。例えば以下のような使い方ができます。


  <Target Name="BatchTarget">
    <ItemGroup>
      <Batch Condition=" '%(Element)' == 'en-US' ">
         <ConditionEnUSBatch>true</ConditionEnUSBatch>
      </Batch>
    </ItemGroup>
    <Message Text="Batch = @(Batch->'%(Element)')" Importance="High" />
  </Target>

出力結果

Batch = en-US;ja-JP


 この時、実は%(Element)だけではなく、%(Identity)というものもBatchの中設定されていてIncludeという要素の値に対応しています。%(Element)がen-USを示す内容のときだけConditionの中で示された比較が成立し、%(ConditionEnUSBatch)もBatchの中にできていて、以下のようにして出力することができます。


 しれっと、まだ学習していない@(ItemGroup)という形式のアイテムグループ変数というものを使いましたが、@()内に書かれた名称と同じ名称のタグが対になっていて、参照していると考えて下さい。今回の場合はBatchタグを参照しています。@(Batch->' ')として、シングルクォーテーションの中で使えるバッチ処理変数を記述すると有効に機能するということです。アイテムグループのその中で有効なバッチ処理変数という意味になります。シングルクォーテーションの中でのバッチはセミコロン区切りで一行の文字列として処理されます。単一の値を取り出すときは、%(Batch.Element)のように記載するとバッチ処理をしめすことができます。バッチ処理が保持する複数値の分だけ処理を繰り返されます。Messageタグの中でバッチ処理変数を%(Batch.Element)のように使うと複数行記述される感じの動きになります。Messageタグの中では1つの項目(今回の場合、Element要素の親タグはBatchしかない)でリストになっているバッチ処理はドットで接続されたバッチ処理変数名を記述して指定する必要があります。


  <Target Name="BatchTarget">
    <ItemGroup>
      <Batch Condition=" '%(Element)' == 'en-US' ">
         <ConditionEnUSBatch>true</ConditionEnUSBatch>
      </Batch>
    </ItemGroup>
    <Message Text="Batch = {@(Batch->' Element = %(Element), Identity = %(Identity), ConditionEnUSBatch = %(ConditionEnUSBatch)')}" Importance="High" />
  </Target>

出力結果

Batch = { Element = en-US, Identity = 1, ContitionEnUSBatch = true; Element = ja-JP, Identity = 2, ContitionEnUSBatch =}


 と表現できることになります。


 さらに、このようにBatchの中のという指定をするために使った@(Batch->' ')を以下のように記述することもできます。


  <Target Name="BatchTarget">
    <ItemGroup>
      <Batch Condition=" '%(Element)' == 'en-US' ">
         <ConditionEnUSBatch>true</ConditionEnUSBatch>
      </Batch>
    </ItemGroup>
    <Message Text="%(Batch.Element)" Importance="High" />
  </Target>

出力結果

en-US
ja-JP


 このように、%(Batch.Element)とすると、バッチ処理の機能が働き、その要素の分だけメッセージ出力処理が繰り返されます。@(Batch->' ')の中にバッチがあった時はその中のメッセージが;区切りで繰り返される感じになります。

 

バッチ処理の例外

 以下のようにTarget内で定義したItemGroupでバッチを呼び出しても評価順序が正しくないため思い通りの結果は得られません。ちゅういして使いましょうとマイクロソフトのWebsiteに記載されていたので、そこは注意したいですね。


  <Target Name="Test" AfterTargets="AfterResourceCompile">
    <ItemGroup>
      <BatchItem Include='a/a.txt' BatchPath='%(Filename)%(Extension)' />
      <BatchItem Include='a/b.txt' BatchPath='%(Filename)%(Extension)' />
      <BatchItem Include='b/a.txt' BatchPath='%(Filename)%(Extension)' />
    </ItemGroup>
    <Message Text="BatchItem=[@(i)]" Importance='High' />
    <Message Text="BatchItem->Include=[@(BatchItem->'%(Identity)')]" Importance='High' />
    <Message Text="BatchItem->BatchPath=[@(BatchItem->'%(BatchPath)')]" Importance='High' />
  </Target>

実行結果

  BatchItem=[a/a.txt;a/b.txt;b/a.txt;b/a.txt]
  BatchItem->Include=[a/a.txt;a/b.txt;b/a.txt;b/a.txt]
  BatchItem->BatchPath=[;b.txt;a.txt;a.txt]


 %(Filename)や%(Extension)ように、定義しなくても使えるバッチというのもあります。が、上記のように、Targetの中でバッチを使うとまだ評価されていないだとかスコープだとかの都合で動作が保証されず思い通りの結果は得られません。


 以下のようにすると期待していたものに近い結果が得られます。


  <ItemGroup>
    <BatchItem Include='a/a.txt' BatchPath='%(Filename)%(Extension)' />
    <BatchItem Include='a/b.txt' BatchPath='%(Filename)%(Extension)' />
    <BatchItem Include='b/a.txt' BatchPath='%(Filename)%(Extension)' />
  </ItemGroup>
  <Target Name="Test" AfterTargets="AfterResourceCompile">
    <Message Text="BatchItem=[@(BatchItem)]" Importance='High' />
    <Message Text="BatchItem->Include=[@(BatchItem->'%(Identity)')]" Importance='High' />
    <Message Text="BatchItem->BatchPath=[@(BatchItem->'%(BatchPath)')]" Importance='High' />
  </Target>

実行結果

  BatchItem=[a/a.txt;a/b.txt;b/a.txt]
  BatchItem->Include=[a/a.txt;a/b.txt;b/a.txt]
  BatchItem->BatchPath=[a.txt;b.txt;a.txt]


 だからといって絶対にTarget内部のItemGroupでバッチ機能を使ってはいけないというわけではなく、以下のように一か所に集約するような使い方をするとうまく動いたりもするので、そういう工夫がわかっている人にはできるというところが、またややこしい。


  <Target Name="Test" AfterTargets="AfterResourceCompile">
    <ItemGroup>
      <preBatch Include='a/a.txt' />
      <preBatch Include='b/b.txt' />
      <preBatch Include='c/c.txt' />
      <Batch Include='@(preBatch)' BatchPath="%(Filename)%(Extension)" />
    </ItemGroup>
    <Message Text="Batch=[@(Batch)]" Importance='High' />
    <Message Text="Batch->BatchPath=[@(Batch->'%(BatchPath)')]" Importance='High' />
  </Target>

出力結果

  Batch=[a/a.txt;b/b.txt;c/c.txt]
  Batch->BatchPath=[a.txt;b.txt;c.txt]

 

バッチ処理の変数で既に定義されている機能

  • %(Filename):
 ファイルの名前部分を取得します。
 例: file.txt の場合、file を返します。
  • %(Extension):
 ファイルの拡張子部分を取得します。
 例: file.txt の場合、.txt を返します。
  • %(RecursiveDir):
 再帰的に検索されたファイルのディレクトリパスを取得します。
 例: C:\project\src\file.txt の場合、src\ を返します。
  • %(Identity):
 項目の識別子(通常はファイルパス)を取得します。
 例: @(Compile->'%(Identity)') は、Compileアイテムのファイルパスを取得します。
  • %(ModifiedTime):
 ファイルの最終更新日時を取得します。
 例: @(Compile->'%(ModifiedTime)') は、Compileアイテムの最終更新日時を取得します。
  • %(CreatedTime):
 ファイルの作成日時を取得します。
 例: @(Compile->'%(CreatedTime)') は、Compileアイテムの作成日時を取得します。
  • %(FullPath):
 ファイルのフルパスを取得します。
 例: @(Compile->'%(FullPath)') は、Compileアイテムのフルパスを取得します。

 

ターゲットバッチ処理とバッチ処理の違い

 vcxprojファイルのあるフォルダに以下のようにファイルを配置します。

  • A\1.txt, A\2.txt
  • B\1.txt, B\2.txt

 このように設定した状態でTargetをバッチ処理をするためのスクリプトを作ろうと思います。まずはディレクトリ名のバッチ処理変数を構成するために、以下のようにスクリプトを書きます。


<Project ...>
  <ItemGroup>
    <TextFiles Include="$(MSBuildThisFileDirectory)*\*.txt"/>
    <TextDirs Include="@(TextFiles->'%(RecursiveDir)')"/>
  </ItemGroup>
  <Target Name="Test" AfterTargets="AfterResourceCompile">
    <Message Text="%(TextDirs.Identity)" />
  </Target>
</Project>
</syntaxhighlihgt>

出力結果
<syntaxhighlight lang="text">
TEST:
A\
B\


 2つのディレクトリを使っているので2行のバッチが組まれています。これをターゲットバッチにするには以下のようにTargetタグの中でOutputsという要素を付け足すとターゲットが2回呼ばれるようになります。


<Project ...>
  <ItemGroup>
    <TextFiles Include="$(MSBuildThisFileDirectory)*\*.txt"/>
    <TextDirs Include="@(TextFiles->'%(RecursiveDir)')"/>
  </ItemGroup>
  <Target Name="Test" AfterTargets="AfterResourceCompile" Outputs="%(TextDirs.Identity)">
    <Message Text="%(TextDirs.Identity)" />
  </Target>
</Project>

出力結果

TEST:
A\
TEST:
B\


 のようになります。同じバッチを使っているので、Aフォルダを処理するTargetとBフォルダを処理するターゲットのように別れましたが、同じ意味を持つ%(TextFiles.RevursiveDir)をMessageタグで使うと2回のTESTが呼び出されますが、別のバッチとして内部でも2回処理しようとします。以下のようなことです。


<Project ...>
  <ItemGroup>
    <TextFiles Include="$(MSBuildThisFileDirectory)*\*.txt"/>
    <TextDirs Include="@(TextFiles->'%(RecursiveDir)')"/>
  </ItemGroup>
  <Target Name="Test" AfterTargets="AfterResourceCompile" Outputs="%(TextDirs.Identity)">
    <Message Text="%(TextFiles.RecursiveDir)" />
  </Target>
</Project>

出力結果

TEST:
A\
B\
TEST:
A\
B\


 繰り返し処理のバッチ機能の中で同じバッチを記述したら、その選択されているバッチのみが処理されるということです。このようにバッチ処理というのは深い理解が必要になる複雑な機能であることに注意が必要です。$(Property)のように記述するプロパティ変数の方がシンプルな機能だと言えると思います。

 

項目リストとバッチ処理

 項目とバッチの関係をもう一度、確認してみましょう。以下のようになります。ドットで接続する書き方のバッチ処理を使って以下のようにできるのでした。


  • 1つの項目とバッチ処理
<Project ...>
  <ItemGroup>
    <TextItems Include="A\a.txt"><Number>1</Number></TextItems>
    <TextItems Include="A\b.txt"><Number>2</Number></TextItems>
    <TextItems Include="A\c.txt"><Number>3</Number></TextItems>
    <TextItems Include="A\d.txt"><Number>4</Number></TextItems>
    <TextItems Include="A\e.txt"><Number>5</Number></TextItems>
    <TextItems Include="A\f.txt"><Number>6</Number></TextItems>
  </ItemGroup>
  <Target Name="Test" AfterTargets="AfterResourceCompile">
    <Message Text="%(TextItems.Number)" />
  </Target>
  <Target Name="Test2" AfterTargets="AfterResourceCompile">
    <Message Text="%(TextItems.Identity)" />
  </Target>
</Project>

出力結果

Test1:
  1
  2
  3
  4
  5
  6
Test2:
  A\a.txt
  A\b.txt
  A\c.txt
  A\d.txt
  A\e.txt
  A\f.txt


<Project ...>
  <ItemGroup>
    <TextItems Include="A\a.txt"><Number>1</Number></TextItems>
    <TextItems Include="A\b.txt"><Number>2</Number></TextItems>
    <TextItems Include="A\c.txt"><Number>3</Number></TextItems>
    <TextItems Include="A\d.txt"><Number>4</Number></TextItems>
    <TextItems Include="A\e.txt"><Number>5</Number></TextItems>
    <TextItems Include="A\f.txt"><Number>6</Number></TextItems>
  </ItemGroup>
  <Target Name="Test" AfterTargets="AfterResourceCompile">
    <Message Text="%(TextItems.Identity) %(TextItems.Number)" />
  </Target>
</Project>

出力結果

Test1:
  A\a.txt 1
  A\b.txt 2
  A\c.txt 3
  A\d.txt 4
  A\e.txt 5
  A\f.txt 6


 ひとつのバッチが複数の項目リストで記述されていると、ドットでつなげるバッチは2つに分かれます。


  • 複数項目とバッチ処理
<Project ...>
  <ItemGroup>
    <TextItems1 Include="A\a.txt"><Number>1</Number></TextItems1>
    <TextItems1 Include="A\b.txt"><Number>2</Number></TextItems1>
    <TextItems1 Include="A\c.txt"><Number>3</Number></TextItems1>
    <TextItems2 Include="A\d.txt"><Number>4</Number></TextItems2>
    <TextItems2 Include="A\e.txt"><Number>5</Number></TextItems2>
    <TextItems2 Include="A\f.txt"><Number>6</Number></TextItems2>
  </ItemGroup>
  <Target Name="Test" AfterTargets="AfterResourceCompile">
    <Message Text="%(TextItems1.Number) %(TextItems2.Number)" />
  </Target>
</Project>

出力結果

Test1:
  1
  2
  3
   4
   5
   6


 %(TextItems1.Number)を展開しているときの中に%(TextItems2.Number)の値は定義されていないので最初の3行では%(TextItems1.Number)の値だけが展開され、のこりの4~6行では、%(TextItems2.Number)が展開されているものの%(TextItems1.Number)の値は存在しないという感じになります。


 この状態を活用して、以下のように記述することができます。


<Project ...>
  <ItemGroup>
    <TextItems1 Include="A\a.txt"><Number>1</Number></TextItems1>
    <TextItems1 Include="A\b.txt"><Number>2</Number></TextItems1>
    <TextItems1 Include="A\c.txt"><Number>3</Number></TextItems1>
    <TextItems2 Include="A\d.txt"><Number>4</Number></TextItems2>
    <TextItems2 Include="A\e.txt"><Number>5</Number></TextItems2>
    <TextItems2 Include="A\f.txt"><Number>6</Number></TextItems2>
  </ItemGroup>
  <Target Name="Test" AfterTargets="AfterResourceCompile">
    <Message Text="Number:%(Number) TextItems1:@(TextItems1) TextItems2:@(TextItems2)" />
  </Target>
</Project>

出力結果

Test1:
  Number:1 TextItems1:A\a.txt TextItems2:
  Number:2 TextItems1:A\b.txt TextItems2:
  Number:3 TextItems1:A\c.txt TextItems2:
  Number:4 TextItems1: TextItems2:A\d.txt
  Number:5 TextItems1: TextItems2:A\e.txt
  Number:6 TextItems1: TextItems2:A\f.txt


 とこのように、ドットでタグを指定しないで、同じ行で対応するアイテムグループを@(ItemGroup)のような形式で指定することで出力機能が働くようになります。今回の場合はNumber:%(Number) TextItems1:@(TextItems1) TextItems2:@(TextItems2)のように指定しました。%(Number)だけを指定した出力はできないことに注意して下さい。

 

バッチ処理によるグループ分け

 以下のようにするとグループ分けが出来ます。


<Project ...>
  <ItemGroup>
    <TextItems1 Include="A\a.txt"><Number>1</Number></TextItems1>
    <TextItems1 Include="A\a.txt"><Number>2</Number></TextItems1>
    <TextItems1 Include="A\c.txt"><Number>3</Number></TextItems1>
  </ItemGroup>
  <Target Name="Test1" AfterTargets="AfterResourceCompile">
    <Message Text="Number:@(TextItems1->'%(Identity) Number:%(Number)')" Condition="'%(Identity)' != ''" />
  </Target>
</Project>

出力結果

Test1:
  Number:A\a.txt Number:1;A\a.txt Number:2
  Number:A\c.txt Number:3


 という感じで分けることが出来ます。こんな感じで%(Batch)のような形式のバッチ処理はドットで接続して使うか、@(ItemGroup)と合わせて使うか、@(ItemGroup->' %(Batch) ')のシングルクォーテーションの中で使うというような利用をするとしっくり使えると言えます。実際のコンパイル処理で活用できるようにまでなれれば、ややこしいバッチ処理も極めたと言えますね。

 

VC PlusPlus:MSBuildに戻る。