Buildless Angular app in single HTML file

概要

Skypack で提供されている ESM 形式の npm パッケージを、ブラウザの JavaScript から直接 import することによって Anguar を動作させることができた。

www.skypack.dev

実際のコード

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Buildless Angular app</title>
    <script type="module">
      import "https://cdn.skypack.dev/zone.js";
      import { Subject } from "https://cdn.skypack.dev/rxjs@7";
      import {
        filter,
        share,
        switchMap,
      } from "https://cdn.skypack.dev/rxjs@7/operators";
      import {
        Component,
        Inject,
        Injectable,
        NgModule
      } from "https://cdn.skypack.dev/@angular/core@12";
      import {
        BrowserModule
      } from "https://cdn.skypack.dev/@angular/platform-browser@12";
      import {
        platformBrowserDynamic
      } from "https://cdn.skypack.dev/@angular/platform-browser-dynamic@12";
      import {
        HttpClient,
        HttpClientModule,
      } from "https://cdn.skypack.dev/@angular/common@12/http";

      class AppService {
        static annotations = [new Injectable()];

        static parameters = [[new Inject(HttpClient)]];
        constructor(httpClient) {
          this.httpClient = httpClient;
        }

        static base = "https://jsonplaceholder.typicode.com";

        getUsers() {
          return this.httpClient.get(`${AppService.base}/users`);
        }

        getPosts(userId) {
          return this.httpClient.get(`${AppService.base}/posts`, {
            params: { userId },
          });
        }
      }

      class AppComponent {
        static annotations = [
          new Component({
            selector: "app-root",
            template: `
              <h4>Show Posts</h4>
              <label>
                Select User: 
                <select (change)="selectedUserId$.next($event.target.value)">
                  <option></option>
                  <option *ngFor="let user of users$ | async" [value]="user.id">
                    @{{user.username}}: {{user.name}}
                  </option>
                </select>
              </label>
              <ul>
                <li *ngFor="let post of posts$ | async">
                  {{post.title}}
                </li>
              </ul>
              <p>
                Powered by
                <a href="https://jsonplaceholder.typicode.com/" target="_blank">
                  JSONPlaceholder
                </a>
              </p>
            `,
          }),
        ];

        static parameters = [[new Inject(AppService)]];
        constructor(appService) {
          this.appService = appService;
          this.users$ = appService.getUsers().pipe(share());
        }

        selectedUserId$ = new Subject();

        posts$ = this.selectedUserId$.pipe(
          filter((userId) => userId > 0),
          switchMap((userId) => this.appService.getPosts(userId)),
          share()
        );
      }

      class AppModule {
        static annotations = [
          new NgModule({
            declarations: [AppComponent],
            imports: [BrowserModule, HttpClientModule],
            providers: [AppService],
            bootstrap: [AppComponent],
          }),
        ];
      }

      await platformBrowserDynamic().bootstrapModule(AppModule);
    </script>
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

Type Challenges 攻略ヒント集

仕事仲間から面白いサイトを教えてもらった。お題に沿ったTypeScriptの型定義を作る問題集のようなサイトだ。

tsch.js.org

TypeScriptの型定義について知らなかったことが多々あったため、攻略テクニックとして以下にまとめる。

配列型、タプル型をマップする

[string, number] 型から [string[], number[]] 型を得る型を定義したいとする。

// object を定義しそうな見た目だが、結果は配列型 or タプル型になる
type ArraysOfArray<T extends any[]> = {
  [K in keyof T]: T[K][]
};

ユニオン型をマップする

string | number 型から string[] | number[] 型を得る型を定義したいとする。

// NG例 ... Tはユニオン型のまま扱われるため (string | number)[] になる
type ArraysOfUnion<T> = T[];

// OK例
type ArraysOfUnion<T> = T extends T ? T[] : never;
type ArraysOfUnion<T extends keyof any> = { [K in T]: K[] }[T];

参考: 296 - Permutation (with explanations) · Issue #614 · type-challenges/type-challenges · GitHub

プロパティを値でフィルタする

値が string 型であるプロパティのみを抽出したオブジェクトの型を得る型を定義したいとする。

type ExtractStringProperty<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
};

参考: TypeScript: Documentation - Mapped Types

インターセクション型を集約する

インターセクション型を用いて { a: number; b: string } を得る型を定義したいとする。

// NG例 ... 実用上は正解だがインターセクション型のままでは不正解となる
type AB = { a: number } & { b: string };

// OK例
type AB = Omit<{ a: number } & { b: string }, never>;

型パラメータが never であるかどうかを判定する

// NG例 ... T が never の場合、結果は true ではなく never になる
type IsNever<T> = T extends never ? true : false;

// OK例
type IsNever<T> = [T] extends [never] ? true : false;
type IsNever<T> = T[] extends never[] ? true : false;

TypeScriptの`infer`を一撃で理解する

準備運動

たとえば「Foo型要素を持つ配列」や「Foo型でfulfillされるPromise」、「Foo型のプロパティ"abc"を持つオブジェクト」の型を定義しようとすると下記のようになる。

type FooArray = Foo[];
type FooPromise = Promise<Foo>;
type FooContainer = { abc: Foo };

この記事にたどり着いた人であればここまでは理解できるはず。

inferの使いどころ

では逆に、「何かの型を持つ配列」や「何かの型でfulfillされるPromise」、「何かの型のプロパティ"abc"を持つオブジェクト」の「何かの型」を得たい場合はどうするか。
ここでinferの出番となる。

// 注意: このコードは型安全ではありません。
type ArrayOf<T> = T extends (infer U)[] ? U : any;
type PromiseOf<T> = T extends Promise<infer U> ? U : never;
type AbcType<T> = T extends { abc: infer U } ? U : unknown;

ここのinfer Uという表現で取り出したい型にUという名前を付け、後から参照している。
T extends ... ? ... : ... は条件演算子のように型を選択するのに使用するが、inferはこのextendsの条件部分でなければ使用することができない。
この例の場合、false側のほうの型は使わないのでanyでもneverでもなんでもいい。

型安全なinfer

Tの型には静的型チェックの制限をかけるのが通例である。
前述の例だとTの型次第ではfalseの条件に合致してしまうため、意図せずfalse側のほうの型になってしまう可能性がある。

type ArrayOf<T extends any[]> = T extends (infer U)[] ? U : any;
type PromiseOf<T extends Promise<any>> = T extends Promise<infer U> ? U : never;
type AbcType<T extends { abc: any }> = T extends { abc: infer U } ? U : unknown;

実用例

TypeScriptにはいくつか事前定義された型があり、その中に「関数の引数の型をタプルとして取得する型」Parameters<Type>や「関数の戻り値の型を取得する型」ReturnType<Type>がある。
これらはinferを用いることで実現しているが、実際のコードはこの記事の内容を理解しただけで書けるような単純なものになっている。
どんなコードかを予想してから下記リンク先から確かめてみると理解が深まるかもしれない。

TypeScript/lib.es5.d.ts at v4.0.2 · microsoft/TypeScript · GitHub

まとめ

  • inferは、ある型の中に含まれる何かの型を取り出すのに用いる。
  • T extends ... ? ... : ...の条件部分にしか使用できない。
  • 取り出したい型のところにinfer Uを当てはめると、取り出した型にUと名前を付けてtrue側の部分で用いることができる。

Windows標準環境でファイル分割 2020年改

大容量ファイルを分割して小さくする

仕事で取引先に10MB超のデータを送ることになった。
しかし、普通に送るとファイルのサイズが大きすぎてメールに添付して送ることができない。
しかも、会社のパソコンなのでフリーソフトを勝手にインストールすることはできない。
そこで、Windows標準環境でも簡単にファイルを分割して小さくすることができる方法を開発した。

以前、Windows標準環境でファイル分割 - reosablo.blogという記事を書いたが、新型コロナウイルスの影響でファイル分割を何度もすることになりそうなので、リニューアルして利便性の良い改良版を作成した。

今回の記事の方法を用いると右クリックメニューで簡単にファイルを分割することができる。
なお、特別なソフトをインストールする必要は無いので会社のパソコンでも問題なく利用できる。

下準備

この手順で右クリックメニューにファイル分割コマンドを追加する。

① 「メモ帳」を開いて以下の内容をコピペする

' Save this script as ".vbs" file in "shell:sendto" folder.
' cite: https://reosablo.hatenablog.jp/entry/2020/04/05/141821

Size = InputBox("Split File Size? (MB)", WScript.ScriptName, "10")
If IsEmpty(Size) Then WScript.Quit()
Size = CLng(Size * 1048576)
Size = Size - Size Mod 512

Set FileSystem = CreateObject("Scripting.FileSystemObject")
TempDirPath = FileSystem.GetSpecialFolder(2).Path
DdfPath = TempDirPath & "\cabprof.ddf"
FirstFilePath = WScript.Arguments.Unnamed(0)
DiskDirectoryTemplate = FirstFilePath & ".split"
CabinetNameTemplate = FileSystem.GetBaseName(FirstFilePath) & "-*.cab"

Set Ddf = FileSystem.CreateTextFile(DdfPath)
Ddf.WriteLine(".Set RptFileName=NUL")
Ddf.WriteLine(".Set InfFileName=NUL")
Ddf.WriteLine(".Set MaxDiskSize=" & Size)
Ddf.WriteLine(".Set DiskDirectoryTemplate=" & DiskDirectoryTemplate)
Ddf.WriteLine(".Set CabinetNameTemplate=" & CabinetNameTemplate)
For Each FilePath In WScript.Arguments.Unnamed
  Ddf.WriteLine("""" & FilePath & """")
Next
Ddf.Close

Set Shell = CreateObject("WScript.Shell")
Shell.CurrentDirectory = FileSystem.GetParentFolderName(FirstFilePath)
Shell.Run "makecab /f """ & DdfPath & """", , True

FileSystem.DeleteFile(DdfPath)

② 「名前を付けて保存」で、ファイル名に"shell:sendto"と入力する。すると、"SendTo"という名前のフォルダに移動する。
③ ファイル名に「ファイルを分割.vbs」と入力して保存する。

ファイルの分割

① 分割したいファイルを右クリックして「送る」というメニューの中にある「ファイルを分割.vbs」を選択する。

② "Split File Size? (MB)"と聞かれるので分割したいファイルサイズを数字で入力する。全角半角はどちらでもOK、小数点ありでもOK。

分割したいファイルと同じフォルダにファイル名と同じフォルダができているので、 そのフォルダに入っている"ファイル名-1.cab"、"ファイル名-2.cab"...という名前のファイルをすべて小分けして送信すればOK。

"ファイル名-1.cab"をダブルクリックすると中にデータが入っているように見えるはず。
当然ながら、すべての.cabファイルが揃っていなければ中身は取り出せない。

ファイルの結合

先方に送信するメールに下記の文言をコピペするとよい。
念のため、メールで送信する前に自分のパソコンで解凍できることを確認しておくとトラブルを防げるかもしれない。

送信するファイルのサイズが大きいため、いくつかのメールに分割して送信致します。  
お手数ですが下記手順でファイルを結合していただくようお願い申し上げます。  

 ① お送りするメールの添付ファイル「~-1.cab」、「~-2.cab」…をすべて同じフォルダ内に保存してください。  

 ② 「~-1.cab」の中にファイルが入っているように見えるのでダブルクリックで解凍してください。  

お送りした添付ファイルが全て揃っていればこの手順で解凍できます。  
もし解凍中に、何かフォルダを選択する画面が出た場合はファイルが足りませんのでご連絡下さい。

後処理

ファイル分割コマンドが要らなくなったらこの手順で削除できる。

エクスプローラーのアドレスバーに"shell:sendto"と入力する。すると、"SendTo"という名前のフォルダに移動する。
② 「ファイルを分割.vbs」のファイルを削除する。

参考文献

Google Chrome拡張機能のGCM(FCM) API利用例 ~Instance IDもあるよ~

Google Chrome拡張機能の開発でFirebase Cloud Messaging (旧名称: Google Cloud Messaging) API (chrome.gcm, chrome.instanceID) を利用しようしたところ、ウェブ上でほとんどドキュメントが見つからなかったため利用例を公開する。

最小限の動作例

事前にFirebase consoleで作成したプロジェクトの設定→クラウドメッセージングから「サーバーキー」と「送信者ID」を取得しておく。 プロジェクトをcloneしたら"send.js"のYOUR_SERVER_KEYを「サーバーキー」に、"receive.js"のYOUR_SENDER_IDを「送信者ID」に置き換えれば動作するようになる。 「サーバーキー」のほうは秘密鍵になるので注意。

そういえばFCM(GCM)ってどんな動作するんだっけ

前述のプロジェクトで言えばevent page (receive.js)がスマホ、browser action (send.js)がサーバの役割に相当する。 event pageが取得したtokenはchrome.strorageを通じて読み取っている。

クライアントサイド(push通知受信側)の動作

1. tokenを取得する

まずはchrome.instanceID.getTokenでtokenを取得する。 token取得時の引数tokenParamsオブジェクトのscopeプロパティには"GCM"authorizedEntityプロパティには「送信者ID」文字列を指定する。 このtokenはAPI内部で保持されており、よっぽどのことが無い限り変更されることはない。 もしAPI内部のtokenが変更されてしまった場合はchrome.instanceID.onTokenRefreshイベントが 発生するので、捕捉してtokenを取得しなおす必要がある。

2. FCMからのpush通知をlistenする

tokenを取得出来た時点ですでにFCM側から拡張機能へ通知を発行する準備はできているので、拡張機能側で受け取る準備として chrome.gcm.onMessageイベントをlistenする。

3. サーバにtokenを渡す

サーバ側でtokenを受け取るAPIを用意したり、手書きでtokenをメモして伝書鳩に託すなど好きな方法でtokenを渡せばよい。 あとはサーバがtokenを使って通知を発行すればchrome.gcm.onMessageイベント経由でデータを受け取ることができる。

サーバサイド(push通知送信側)の動作

1. tokenを受け取る

伝書鳩からメモを受け取った場合はO(オー)と0(ゼロ)などの読み間違いに注意する。
ここまでくればpush通知を発行する準備はすべてできている。

2. FCMにデータを送信する

気が向いたときに通知データをhttps://fcm.googleapis.com/fcm/send宛にPOST送信する。 HTTPヘッダのAuthorizationフィールドにはkey=に続けて「サーバーキー」を設定する。

API扱ってみた感想

  • chrome.instanceIDでtoken取得したらchrome.gcmにtoken与えて何かしなきゃいけない気がしてたけど全然そんなことなかった。わかりにくいわっ!
  • APIリファレンスに動作例が載っててほしかった。

最終的にこんなもんできました。伝書鳩飛ばすのが難しかったです。

バッチファイルにPowerShellスクリプトを埋め込む 標準入力対応版

PowerShellスクリプトをダブルクリックやドラッグ&ドロップで実行できるようにできるだろうか?と情報を探していたところ、下記のサイトを見つけた。

pf-j.sakura.ne.jp

pf-j.sakura.ne.jp

これを読んでみてもうちょっと改良できそうかも?と思い試行錯誤していろんなものが得られたので記事を起こすことにした。

この記事に含まれるスクリプトは特に著作権表示無く使用してもらって構わない。

改変点

  • 標準入力$inputを扱えるようにした(重要)
  • スクリプトのパスを$PSCommandPath$PSScriptRootから取得できるようにした
  • 最初の1行以外はすべてPowerShellスクリプトを書けるようにした
  • 変態さを増した(変態)

スクリプトの例

フルスペック版

@setlocal enabledelayedexpansion&set a=%*&(if defined a set a=!a:"=\"!&set a=!a:'=''!)&powershell/c $i=$input;iex ('$i^|^&{$PSCommandPath=\"%~f0\";$PSScriptRoot=\"%~dp0";#'+(${%~f0}^|Out-String)+'} '+('!a!'-replace'[$(),;@`{}]','`$0'))&exit/b
# ここからPowerShellスクリプト

"Executing $PSCommandPath..."
"$($args.length) parameter(s) passed"
$args | %{ $i = 0 }{ echo "`$args[$i]: $_"; $i++ }
"`$input: $input"

最初の1行をコピペして拡張子をバッチファイル("cmd"または"bat")にすれば2行目以降に好きなPowerShellスクリプトを書くことができる。

簡易版

制限: PowerShell バージョン3.0以上限定、コマンドライン引数が使用できない、標準入力が使用できない、スクリプトのパスが取得できない

@powershell/c '#'+(gc \"%~f0\"-ra)^|iex&exit/b
# ここからPowerShellスクリプト

"Time: $([DateTime]::Now)"
"Guid: $([guid]::NewGuid())"

たぶんこれが一番短いと思います。

解説 (TL;DR)

設計思想

おまじない的なものをバッチファイルの先頭にぺたっと貼り付けておけば特に何も考えなくてもスクリプトを書ける環境を目指した。

あと、おまじないは目立ってはいけないので保守性よりも文字数削減を優先した。

バッチファイルとしての動作

このスクリプトは1行目だけがバッチファイルとして動作するようになっている。

行頭の@(アットマーク)

スクリプトの実行時にその行だけコマンドをエコーさせない効果を持つ。

@echo offと書けばそれ以降は各行頭に@を書く必要は無くなるのだがecho off↵は11 byteである。 スクリプトが10行に満たない場合は各行頭に@を書く方法の方が文字数を削減できる。

setlocal enabledelayedexpansion&set a=%*

スクリプトに渡されたコマンドライン引数をPowerShellに渡す前の下処理のため環境変数を使用している。 setlocalによって環境変数のスコープをスクリプト単位に限定し、set a=%*によってコマンドライン引数をまるごとaという環境変数に格納する。 また、enabledelayedexpansionは後述する環境変数の遅延展開のため指定している。

&(アンパサンド)

&と改行はどちらもコマンドを区切る効果を持つ。 &は1 byte、改行は@含めて3 byteであるため、どうしても場合以外は&を使う方が文字数を削減する。

(if defined a set a=!a:"=\"!&set a=!a:'=''!)

スクリプトコマンドライン引数が渡されていない場合、set a=%*の結果、環境変数aは未定義状態となる。 環境変数に格納された文字列は%somevar:target=replace%のような文法で置換することができるが、環境変数somevarが未定義の場合結果は空文字列ではなく変な文字列になってしまう。それを防ぐため、環境変数aが定義されているかどうかチェックするためifコマンドを入れた。 なお、ifの文法はちょっと特殊で、後続する&は条件にマッチするときのみ実行されるようになっているのでコマンド全体を()で囲っている。

環境変数の遅延展開のおかげで、本来は改行を挟まなければset a=%*が呼び出されるよりも前の状態で%a%が展開されてしまうが、代わりに!a!を使うことでコマンドの実行時に環境変数を展開することができる。

setコマンドを2回実行しているがこれは"(ダブルクォーテーションマーク)、'(シングルクォーテーションマーク)の2文字をエスケープしている。 前者はバッチファイル側の都合で、コマンドの引数にダブルクォーテーションマークを含む文字列を渡そうとするとバックスラッシュによるエスケープが必要になるためここで変換しておく。 後者はPowerShellスクリプト側の都合で、'を用いた文字列内で'を使用するために''へ変換している。

powershell/c

準備が整ったのでようやくpowershell.exeを実行する。 -Commandオプションによりあとに続く文字列をPowerShellスクリプトを実行することができる。 引数は前方一致で選ばれるためわざわざ-Commandと8 byteも書かなくても-cの2 byteだけで通じる。 また、引数の前の-(ハイフン)は実は/(スラッシュ)でも代用可能であり、空白文字1 byteを節約できるので/を選んだ。

exit/b

バッチファイルを終了するのに/bが必要となるが、ここでもしっかりと/前の空白文字を節約する。 オプション引数でエラーコードを返すことができるが、exitコマンド自体はエラーコードを上書きしないのでexit/b !errorlevel!と書かなくてもちゃんと環境変数errorlevelは保存されている。

exit/bされたあとはバッチファイルとしてはパースされないので何を書いてても問題ない。

なお、goto:eofでもexit/bと同じ効果が得られるが文字数節約のためexitとしている。

PowerShellスクリプトとしての動作

$i=$input;

次に示すInvoke-Expressionでは標準入力$inputを与える方法が無いにも関わらず空文字列で上書きされてしまうので、上書きされる前に$inputの中身を別の変数に退避させた。

iex ('$i^|^&{...'+'} '+('!a!'-replace'[$(),;@`{}]','`$0')

Invoke-Expressionコマンドを使って文字列をPowerShellスクリプトとして実行している。 iexInvoke-Expressionに既定で設定されているエイリアスである。

文字列が示すスクリプトでは、スクリプトブロック{...}を定義して即座にそれを&で実行している。 スクリプトブロック実行時に先ほど退避させた$inputをリダイレクト、引数はバッチファイルの環境変数%a%が展開されたものを一部正規正規置換を施した上で渡している。 スクリプトコマンドライン引数はPowerShellの文法的に意味のある記号の前に-replace演算子`を追加することでがとしてエスケープしている。

$PSCommandPath=\"%~f0\";$PSScriptRoot=\"%~dp0"

PowerShellでは既定のグローバル変数として$PSCommandPathスクリプトファイルへのパス(通常は拡張子".ps1")と、$PSScriptRootにその格納フォルダへのパスが格納されている。 既定のグローバル変数とバッティングするため、スクリプトブロック内のローカル変数として$PSCommandPath$PSScriptRootを定義している。 パス自体はバッチファイル実行時に定義される変数%0をファイルパス形式%~f0と親フォルダパス形式%~dp0から受け取っている。

なお、最後の"だけ\でエスケープしていないのは誤記ではない。%~dp0は最後に\が付く一方で$PSScriptRootには\が付かない。%~dp0の最後に必ず付く\"のエスケープに使用して相殺している。

('...#'+(${%~f0}^|Out-String)+'...')

丸括弧内では${%~f0}という名前の変数から値を読みだしてOut-Stringコマンドレットに渡している。 PowerShellとして実行される際には%~f0はドライブレターから始まる文字に展開されており、バッチファイル自身を読み出すことができる。 ただし、読みだされた結果はGet-Contentコマンドと同様に"1つの文字列"ではなく"文字列の配列"となっているため、 Out-Stringコマンドレットで改行を含む1つの文字列に変換している。

${%~f0}と記述するとgc \"%~f0\"と記述するよりも2文字削減することができる。 ただし、ファイル名本体に丸括弧"{","}"が使えなくなるので注意。

ファイルを読み込む文字列の直前に#を入れ込むことにより、取得したファイル、つまりこのバッチファイル自体の1行目を無視することができる。

なお、(${%~f0}^|Out-String)の部分は次に説明するものに置き換えるとさらに5文字短くなる。

('...#'+(gc \"%~f0\"-ra)+'...') (PowerShell 3.0以上限定)

丸括弧内ではGet-Contentコマンドレットを使用してバッチファイル自身をテキストファイルとして読み込んでいる。 gcGet-Contentに既定で設定されているエイリアスである。 Get-Contentコマンドは標準ではイテレータ?を返すようになっているが-Rawオプションを指定すると文字列を返すようになる。 例によって前方一致で1文字節約している。

旧版解説

2つ目の記事に気付かずに書いたものがあったので差分だけ残しておく。

@('*)2>nul&setlocal enabledelayedexpansion&set a=%*&if defined a set a=!a:`=````!&set a=!a:"=`\"!&set a=!a:$=```$!
@powershell/c $$=$input;iex \"`$$|&{`$PSCommandPath=`\"%~f0`\";`$PSScriptRoot=`\"%~dp0`\";$(gc `\"%~f0`\"-ra)} %a%\"&exit/b
')>$()

('*)2>nul

PowerShellスクリプトの都合上、@('を必ず最初に書かなければならないがバッチファイルにとってはなにもしない記述でなければらない。 バッチファイルにとっても(は複数のコマンドを1つにまとめる効果を持つので必ず)で閉じる必要がある。 2>nulによって標準エラー出力に吐かれるエラーメッセージを闇に葬り去ることができる。

また、Windowsファイルシステムでは'(シングルクォーテーションマーク)はファイル名に使うことができる文字なので@(')2>nulと書いてしまうと万が一'.exeなどの実行ファイルにパスが通っているとそれが実行されてしまうことになる。 副作用をふせぐためファイル名に使用できない*(アスタリスク)を挿入した。

改行

ifコマンドを脱出するため&ではなく改行を使用した。

文字数が同じなので見栄え重視で改行を選んだ。

@('...')>$()

ここからスクリプトファイルをファイルの先頭からPowerShellスクリプトとして読み直す。

@()は配列を定義する文法であり、文字列1つを内包している。 'で挟まれた中身が文字列となるが、改行を含むことができるようになっているためバッチファイルとしての文字列はほとんど無視される。

また、配列を1つ書いただけではプロンプトにその配列自身が表示されることになってしまう。 PowerShellでは$nullにオブジェクトをリダイレクトすることによって配列を闇に葬り去ることができる。 $()は括弧の中に何も書かないと$nullとして評価される。文字数節約のため$()と記載した。

ここからあとはおよそすきなPowerShellスクリプトを書くことができる。

その他

リファレンスなど読まずに解説部分を書いたので色々間違いはあるかもしれない。

Windows標準環境でファイル分割

仕事で取引先から10 MB超のバイナリファイルを受け取ることになった。
しかしながら、双方のメールサーバの制限でファイルサイズ超過のため送ることも受け取ることもできない。 うちの会社が大阪県、取引先が名古屋県なので記録媒体を手渡しすることもできず、ファイルを分割してもらうことにした。

取引先様には怪しいフリーソフトをインストールするようにとは言えないため、Windows標準の機能で分割してもらうこととした。
Windwos XP以上であれば動くはず。

(2020年4月追記)
この記事の方法よりももっと便利な方法の記事を書きました。新しい記事の方法のほうをおすすめします。

ファイルの分割

① 「メモ帳」を開いて下記の内容をコピペする

setlocal
set ddf=%TEMP%\cabprof.ddf
(echo %1) > "%ddf%"
makecab /d MaxDiskSize=1048576 /d RptFileName=NUL /d InfFileName=NUL /d DiskDirectoryTemplate="%~n1" /f "%ddf%"
del "%ddf%"

4行目の"MaxDiskSize=1048576"は分割単位1 MB(1,048,576 Byte)を表しているが、512の倍数なら好きな値に変更できる。
② "splitfile.cmd"という名前で好きな場所に保存する
③ 分割したいファイルを"splitfile.cmd"にドラッグ&ドロップする

分割したいファイルと同じディレクトリにファイル名と同じフォルダができているので、 そのフォルダに入っている"1.cab"、"2.cab"...という名前のファイルをすべて小分けして送信すればOK

ファイルの結合

① 受け取った".cab"ファイルを同じディレクトリに配置する
② "1.cab"にファイルが入っているように見えるので適当に展開先を指定する