技術メモ、Tips、忘備録

技術系のメモ、試してみたこと、その他

【雑記】javaで定数を比較する時の違和感

javaで定数を比較する時によく、

if(CONSTANT.equals(value)){
    System.out.print("hoge");
}

という書き方をします。
CONSTANTは定数でvalueはチェック対象の変数だと思ってください。

これあんまり好きじゃないんですよね。
理由はやりたいこととやっている事(前から読んだ時の意味)が逆になるからです。
上記の例だと、
□やりたいこと: valueCONSTANTと等しい時、hogeを出力

□やっている事: CONSTANTvalueと等しい時、hogeを出力

いやまぁやっていることはどちらも同じなんですが、やりたいことをそのまま表現するには

if(value.equals(CONSTANT)){
    System.out.print("hoge");
}

となっていた方が単純にいいですよね。。
最近は読みやすいプログラムを書くことが重要になってきているので、正直この辺は気持ち悪いですね。

ちなみに定数を前に持ってくるのは、valueがnullだったときのNullPointerExceptionを避けるためです。

【失敗談】Gitのコマンドとオプションを補完するコンソールアプリケーションを作ろうとした話

前置き

gitのコマンドとオプションを補完するコンソールアプリケーションを作ろうとしていましたが、失敗しました。
何がだめだったか、どうすべきだったかを自戒も込めて記事にします。

何がしたかったか

作りたかったものはタイトルの通りです。
手順としては、
1. gitの公式サイトのコマンド一覧からコマンドとオプションをスクレイピングする
2. 1でスクレイピングしたものをもとに、補完するための仕組みを作成する。
です。

コマンド、オプション、synopsisのスクレイプ

まず、スクレイプするにあたり以下のことを考えなければいけません。
1. 対象のコマンドは?
2. スクレイプのルールは?

1については、command_list.txt公式サイトの一覧をベースに、いらないもの(コマンドではないもの等)を除外してスクレイプ対象のコマンドを決めました。

2について、それぞれ以下のように決めました。
■ URL & command
https://git-scm.com/docs/git-{command}」という形式でコマンドごとのリファレンスがあります。
例えばaddコマンドだとhttps://git-scm.com/docs/git-addといった感じです。

■ synopsis
ざっくりいうと、コマンドごとのリファレンスのHTMLを見ると、シノプシス_synopsisというidに属しています。

■ options
ざっくりいうと、コマンドごとのリファレンスのHTMLを見ると、オプションはすべて_optionsというidとhdlist1というクラスに属していることがわかります。

ここまでそろえば簡単ですね。...と思っていましたが実は少しめんどくさかったです。

スクレイプで躓いたこと

結論から言うと、スクレイプのルールの前提が甘かったです。甘かったといってもほとんどはルール通りスクレイプできましたが、一部のドキュメントがルールに沿わない構造となっていたため、個別ルールを追加してスクレイプしました。
そもそもの目標の1つとして「gitのアップデートに柔軟に対応する」というのがありました。
しかし、上記のような「例外(ルールを外れたスクレイプ)」を許してしまうと、gitのアップデートのたびにスクレイプ用のアプリを改修しなければいけない可能性があるので、あまりよくない状況になりました。

ともあれスクレイプはひとまず完了

自分のプロジェクトに実際にスクレイプしたものがあります。ご参考までに

スクレイプしたものを解析する。

上のセクションで、synopsisもスクレイプしました。このセクションで使うためです。
まずはそれぞれのオプションがどのように使われるかを知る必要があります。そこで、synopsisを解析し、オプション一つ一つにメタデータ(値を持つか、他のオプションと同時に指定できるか、等)を付けようと考えました。

sinopsis解析のパターン

まずはsynopsisの記法についてです。
調べてみましたが、あまり参考になるものを見つけられませんでした。正規表現の概念に依存するというようなことはあったのですが.....

まぁでも、何となくsynopsisの記法は直感的に理解していたので、適当にやってみるか!となりました。

...が、これが思ったより難しい。当たり前ですが、人が見れば普通に分かることでも、機械的に解析しようと思うと大変です。解析するにあたり、いろいろ考えなければいけませんでした。最も頭を悩まされたことは「そもそもコマンドによってsynopsisの書き方のクセが異なる」ということでした。
またスクレイプした時のように個別ルールが必要になるのでは…
まさにそうでした。1つの共通ルールでの解析は(私の能力では)無理そうでした。

だんだんと当初の考えとずれてきたので、1度原点回帰することに。

そもそも前提が間違っていた

当時(今もですが)、Powershellの補完機能に魅了されてました。従来のshellは「ヘルプを見て、すべてタイピングする」というのが基本ですが、Powershellは「途中までタイプすれば保管してくれる」ので初めてでも割と感覚だけで使えました。また特性としてコマンドが「動詞-名詞」の形式かつ、オプションも正式な英語(従来のshellのように略語ではない)なので、コマンド名、オプション名から何がしたいかを予測可能なのです。
例えば、PowershellGet-ChildItem - Fileとcmd.exeのdir /a-dは同じ挙動をします。Powershell、cmd.exeを知らない人(例えば開発者ではない事務の人とか)が上記を見た場合、前者は「あぁ、なんか子アイテムのファイル取ろうとしてるわ」と容易に予想できますが、後者は予測不可能でしょう。

従来のshellの場合、その特性から「コマンド名、オプション名は短く、タイピングしやすいこと」が重要な要素だったと考えます。現にgitも主要なオプションに短い形式が用意されていました。 一方でPowershellは「わかりやすさ」に重きを置いている気がします。なので、逆に普通にシェルとしてプロンプトから利用するのは少し面倒くささもあります。(エイリアス使えば別ですが)

上記を踏まえて、「もともとタイピングを減らすために設計されているので初見では分かりにくいものを補完したところで、補完されたものの意味を予測できないので結局ヘルプ見るしかない」ことに考えが及ばなかったことが敗因だと思いました。

また、「GitのHelpページはひな形こそあれど、別に厳密なルールで構造化されているわけではない」、「synopsisも別に明確なルールがあるわけではなく、人にわかりやすい記法(≒機械にはわかりにくい)である」ということに気づけなかったのも良くなかったと思います。たしかにgit開発のチュートリアルとか見てもそんな感じですね。。

結局、ここで考えるのをやめて、アプリ作りも終わりました。 

結論

今回のことで、
■ 公式ドキュメントを読め。
■ 根拠のない決めつけで物事を進めるな

ということを痛感させられました。ただ、経験としては良かったです。

ここまで読んでいただきありがとうございました。

Powershell Coreをビルドしてデバッグする。

前置き

最近Powershellを使うことが多かったのですが、なかなか使いこなすのが難しいので、内部仕様を見て理解を深めようと思ったのでやってみます。
Powershell CoreはWindows Powershellコードベースにフォークされたプロジェクトです。
ビルドはwindows-core.mdにそってやります。

環境

Windows 10 (1909)
Visual Studio Community 2019 (16.7.2)

ビルド

visual studioの設定

[ツール] -> [ツールと機能を取得] -> [ワークロード]
[.NET デスクトップ開発]と[.NET Coreクロスプラットフォームの開発]にチェックしてインストールする。

リポジトリのクローン

今回は現在の最新のタグ(v7.1.0-preview.6)でやろうと思います。
(各種タグについての説明はここで確認できます)

$git clone https://github.com/PowerShell/PowerShell.git
$git checkout v7.1.0-preview.6
■.NET Core SDKのインストール(またはglobal.jsonの書き換え)

現在.NET SDK 5.0以降をインストールしていない人はこのセクションの手順は実施不要です。
後述する「Powershell Coreのビルド」で自動的に.NET SDKがインストールされます。
.NET Core SDKをすでにインストール済みの人は、この手順が必要です。

  • インストールされている.NET SDKのバージョン確認
$dotnet --list-sdks
3.1.301 [C:\Program Files\dotnet\sdk]
3.1.401 [C:\Program Files\dotnet\sdk]
5.0.100-preview.7.20366.6 [C:\Program Files\dotnet\sdk]

自分の場合は5.0.100-preview.7.20366.6ですね。

  • \global.jsonを書き換え
    cloneしたPowershell直下の.\global.jsonを下記のように書き換えます。
$type global.json
{
  "sdk": {
    "version": "5.0.100-preview.7.20366.6"
  }
}
Powershell Coreのビルド

cloneしたPowershell直下で実行してください。 Powershellまたはpwshで実行してください。

Powershellのプロンプトってどう表現すればいいか分からなかったので、ここではPS>としています 。

以下のコマンドで.NET SDKをインストールします。すでにインストール済みの場合はdotnet is already installed. Skipping installation.のようなメッセージが出ます。

PS>Import-Module .\build.psm1
PS>Start-PSBootstrap

以下のコマンドでPowershell Coreをビルドします。

Start-PSBuild

正常終了されればOKです。
実行ファイルへのパスは下記のコマンドで確認できます。

PS>Get-PSOutput
C:\develop\global_reps\PowerShell\src\powershell-win-core\bin\Debug\net5.0\win7-x64\publish\pwsh.exe

Visul Studioでデバッグ

cloneしたPowershell直下の.\PowerShell.slnを開きます。 f5キーで実行します。
プロンプトが表示されるので、試しにGet-ChildItemコマンドレットをたたいてみます。

PS C:\develop\global_reps\PowerShell\src\powershell-win-core\bin\Debug\net5.0> Get-ChildItem

    Directory: C:\develop\global_reps\PowerShell\src\powershell-win-core\bin\Debug\net5.0

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/08/22    10:07                cs
d----          2020/08/22    10:07                de
d----          2020/08/22    10:07                en-US
d----          2020/08/22    10:07                es
d----          2020/08/22    10:07                fr
d----          2020/08/22    10:07                it
d----          2020/08/22    10:07                ja
d----          2020/08/22    10:07                ko
d----          2020/08/22    10:07                Modules
d----          2020/08/22    10:07                pl
d----          2020/08/22    10:07                preview
d----          2020/08/22    10:07                pt-BR
d----          2020/08/22    10:07                ru
d----          2020/08/22    10:07                runtimes
d----          2020/08/22    10:07                Schemas
d----          2020/08/22    10:07                tr
d----          2020/08/21    16:19                win7-x64
d----          2020/08/22    10:07                zh-Hans
d----          2020/08/22    10:07                zh-Hant
-a---          2020/08/21    16:13           8933 Install-PowerShellRemoting.ps1
-a---          2020/08/21    16:13           2908 InstallPSCorePolicyDefinitions.ps1
-a---          2020/08/21    16:13           1095 LICENSE.txt
-a---          2020/04/18    16:34         403456 Markdig.Signed.dll
-a---          2020/04/22    16:37         350072 Microsoft.ApplicationInsights.dll
-a---          2020/07/25     4:15        5437320 Microsoft.CodeAnalysis.CSharp.dll
.......
PS C:\develop\global_reps\PowerShell\src\powershell-win-core\bin\Debug\net5.0>

大丈夫そうですね。

コマンドの実行エントリ

.\src\Microsoft.PowerShell.ConsoleHost\host\msh\ConsoleHost.csの2583行目当たりの
_exec.ExecuteCommand(line, out e, Executor.ExecutionOptions.AddOutputter | Executor.ExecutionOptions.AddToHistory);
からコマンドを実行します。引数のlineには(よく確認してないけどたぶん、、)プロンプトからの入力がそのまま入る感じですね。

ブレイクポイント

プロンプトの実行、コマンドの実行等の主要なポイントでブレイクポイントを用意しました。
ここからダウンロードしてVisual Studioのブレイクポイントウィンドウからインポートして下さい。

読んでいただきありがとうございました。

階層が深いディレクトリから同名ファイルを探し出して上書きする

前置き

仕事中にタイトルのようなことがしたいことがありました。
例えば、普段の作業はローカルで行い、ファイルサーバにコピーするときに、エクスプローラでいちいちファイルの場所まで開く必要があります。
別に大した手間ではないのですが、よく考えると面倒だし、なんでファイルのパスまで覚えとかないといけないんだろう。。。と思ってしまったので、それ用の関数を用意することにしました。

スクリプト記述

注意 ここではローカルパスからローカルパスのコピーを行います。前置きでファイルサーバへのコピーと書きましたが、一般的にはそういう使い方の方が多いのかな?と思ってそう書きました。私の実際の用途としてはデプロイ資材の差し替えとかです。

早速Powershellで書いていきます、

function Global:Copy-FileToSameName
{
    param(
        [Parameter(Mandatory, HelpMessage="送り側")]
        [string]$source,

        [Parameter(Mandatory,HelpMessage="受け側")]
        [string]$destination
    )
    # is Exist ?
    $_source = [System.IO.FileInfo]::new("$(Convert-Path -Path $source)")
    $_destination = [System.IO.DirectoryInfo]::new("$(Convert-Path -Path $destination)")
    $file_list = [System.IO.Directory]::GetFiles(
        $_destination.FullName.Split("`""),
        $_source.Name.Split("`""),
        [System.IO.SearchOption]::AllDirectories)

    foreach ($item in $file_list)
    {
        if ($item -ne $_source.FullName.Split("`""))
        {
            Copy-Item -Path $_source.FullName.Split("`"") -Destination $item

        }
    }
}

こんな感じでしょうか。ちなみに引数でFileInfoやDirectoyrInfo型ではなくstring型で受けたのは理由があります。
それはまた今度記事にしようと思います。

動作確認

  1. コピー元ファイルが存在するディレクトリで、そのディレクトリの配下にある別の同名ファイルに上書き
  2. コピー元ファイルが存在しないディレクトリで、そのディレクトリの配下にある別の同名ファイルに上書き
  3. コピー先が複数存在する場合
    こんな感じでしょうか。
    ではまず準備です。以下のような構造でディレクトリを用意します。
C:\DEVELOP\TMP_SCRIPT\SCRIPT\1\TEST
├─1
│  │  file1.txt
│  │
│  └─parent
│      ├─child
│      │      file1.txt
│      │      file2.txt
│      │
│      └─child2
│              file3.txt
│              file4.txt
│
├─2
│  └─parent
│      ├─child
│      │      file1.txt
│      │      file2.txt
│      │
│      └─child2
│              file3.txt
│              file4.txt
│
└─3
    └─parent
        ├─child
        │      file1.txt
        │      file2.txt
        │
        └─child2
                file1.txt
                file4.txt

ファイルの更新確認は、gitで差分確認しましょう。
現在の状態です。差分はないです。
f:id:monkey999por:20200828113234p:plain
実行してみます。

Copy-FileToSameName -source .\test\1\file1.txt -destination .\test\1\
Copy-FileToSameName -source .\file1.txt -destination .\test\2\
Copy-FileToSameName -source .\file1.txt -destination .\test\3\

f:id:monkey999por:20200828113732p:plain
大丈夫そうですね。 ファイルの中身も確認して正常にコピーされていました。(画像だらけになるので張りません、、)
今回のスクリプト等は、tmp_scriptに置いてあります。
読んでいただきありがとうございました。

【Powershell】任意のアプリのウィンドウサイズ等を指定して起動する

前置き

GUIアプリケーションを実行すると、基本的には最後に閉じた状態で開きます。
ニッチな需要だと思いますが、例えばGUIアプリを利用したシェルスクリプトではウィンドウを隠したり、特定の条件のときだけアプリをフルサイズで開きたい、ということがあるかもしれません。
(ちなみに私は仕事中にそういうことがありました。) そこで、その方法について記載します。

スクリプト記述

※注意
コンソールのコマンド実行を説明するときは、習慣的に$がつけられることが多いです。(例えば$dirと書かれていたらコンソールでdirと打つ)
powershellでは変数を定義するときに、$varのように書くとvarという名前の変数を定義できます。
なので、ここで紹介するスクリプト$を含めて実行してください。

それでは、いきなりですがシェルはこちら

$p =New-Object -TypeName System.Diagnostics.ProcessStartInfo
$p.FileName ="{プログラム名}"
$p.WindowStyle =[System.Diagnostics.ProcessWindowStyle]::{設定値}
[System.Diagnostics.Process]::Start($p)

シェル自体の説明は割愛します。

  • {プログラム名}には任意のファイルを指定できます。
    例えば$env:LOCALAPPDATA\Programs\Microsoft VS Code\bin\codeや、パスが通っていればnotepadのように名前だけの指定もできます。
    また、hoge.txtのように実行ファイル以外を指定した場合は、その拡張子に紐づいたアプリが実行されます。(.txtだとnotepad.exe等)

  • {設定値}にはSystem.Diagnostics.ProcessWindowStyle型の値を指定できます。
    具体的には以下のいずれかを指定できます。

Hidden Maximized Minimized Normal
非表示 最大化 最小化 通常(最後に閉じたときの状態)
動作確認

試しにメモ帳をサイズ最大で起動してみましょう

PS C:\> $p =New-Object -TypeName System.Diagnostics.ProcessStartInfo
>> $p.FileName ="notepad"
>> $p.WindowStyle =[System.Diagnostics.ProcessWindowStyle]::Maximized
>> [System.Diagnostics.Process]::Start($p)                                                                              
Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
     28       5     1032       2188       0.02   1132   1 notepad

f:id:monkey999por:20200826233521p:plain
大丈夫そうですね。
読んでいただきありがとうございました。

Windowsの環境変数「PATH」

初めに

PATHについての説明はいくらでも転がっているのですが、(いいことですが)丁寧な説明で長い記事になっているものが多く、読むのが面倒!という人のために「何ができるか」「なぜ必要なのか」を簡潔にまとめます。
それではどうぞ

PATHを通すことでできること

  • プログラムを名前だけでどこからでも実行することができる
    例:$notepad -> メモ帳()が起動します

Pathが通ったプログラムを実行するときの内部的な動き

説明

ファイル名だけをコンソールから実行した場合、

登録してあるPATHの直下にある、拡張子にPATHEXTを持つファイルを実行する
という動きになります。 また、PATHやPATHEXTは登録されている順にファイル検索が行われ、最初に一致したものを実行します。(※後で補足します)

確認

試しに、メモ帳(notepad.exe)を起動します。

$notepad

はい、起動しますね。
ちなみにnotepad.exeは二種類の実行ファイルがあります。(私の環境だけ。。?)

$where notepad
C:\Windows\System32\notepad.exe
C:\Windows\notepad.exe

$whereは検索パターンに一致するファイルの場所を表示するコマンドです。
既定では、現在のディレクトリおよび PATH 環境変数によって指定されたパス内を検索します。
先ほど実行したnotepad.exeはC:\Windows\System32\notepad.exeの方ですね。
C:\Windows\notepad.exeを実行するには、フルパスで指定してやるか、C:\Windows\の直下で$notepadを実行する必要があります。

そもそもなぜPathが必要なのか?

一つは、冒頭でも書いた通り「ファイル名だけでどこからでも実行できると便利」ということが挙げられます。
そのほかの理由として、考えられるのは、アプリケーションの開発者側が、ユーザのコンピュータのアプリケーションを実行する際にアプリケーションがどこにあるのかを意識しなくてよい、というのもあるような気がしますね。

余談

コマンドプロンプトから実行できるコマンドは何があるんだろう?」
そんな時に自分の環境でファイル名のみで利用可能なコマンドの一覧を確認したければ、powershell

foreach ($a in ($env:PATHEXT).Split(";"))
    {Start-Process -FilePath "cmd" -ArgumentList " /c where *$a >> %userprofile%\Downloads\command.txt" -Wait
}

を実行すると、%userprofile%\Downloads\command.txtに使用可能なコマンドの一覧が出力されます。
※正確には、上記で出力できるのは外部コマンドと呼ばれる類のものです。cmd.exeには組み込みコマンドというものもあり、組み込みコマンドはコマンドプロンプトhelpと打てば全量が表示されます。

Gitでコマンドにalias(=別名)を設定し、爆速でコマンドをたたく

前提

gitについてある程度知っている前提です。 gitとはそもそも何か?ということについてはこちらをご覧ください

なぜalias(=別名)が必要か

例えば、 コンソールでgitを使用している人が一番使うであろうコマンドのこちら

$git status

リポジトリの状態を確認するコマンドですが、ほんとによく使います、、
$git addから$git pushまでの間に3,4回使ったりすることも稀によくあると思います。
使用頻度が高いくせして、'status'と6文字もタイピングする必要があるのは、なかなかめんどくさい。
そこで、別名を付けることで、タイピング量を減らしてやろう!というのが目的です。
それでは見ていきましょう。

aliasの設定

※ 前提(補足)

gitの設定にはスコープ(=設定の有効範囲)があり、次の3つに分かれます。
スコープについての詳しい説明はこちらが大変参考になります。

system global local
システム単位 ユーザ単位 リポジトリ単位

今回はglobalスコープにaliasを設定します。

alias設定

ここでは私も使用している設定を例として挙げます。
ちなみにタイトルでは「コマンドにaliasをつける」と言っていますが、実際はコマンド以外にもつけられます。

$git config --global alias.a "add" 
$git config --global alias.b "branch"
$git config --global alias.c "checkout"
$git config --global alias.ch "cherry-pick"
$git config --global alias.d "diff"
$git config --global alias.l "log --graph --all --format='%%x09%%C(cyan bold)%%an%%Creset%%x09%%C(yellow)%%h%%Creset %%C(magenta reverse)%%d%%Creset%%s' --branches"
$git config --global alias.rmh "reset --mixed head"
$git config --global alias.rhh "reset --hard head"
$git config --global alias.s "status"

例だと、statusにはsというaliasを付けています。
設定は%userprofile%\.gitconfigに下記のように反映されます。

[alias]
        a = add
        b = branch
        c = checkout
        ch = cherry-pick
        d = diff
        l = log --graph --all --format='%x09%C(cyan bold)%an%Creset%x09%C(yellow)%h%Creset %C(magenta reverse)%d%Creset %s' --branches
        rmh = reset --mixed head
        rhh = reset --hard head
        s = status

動作確認

gitリポジトリで、$git status$git sを試してみます。
同じ結果が得られればOKです。

$git status
On branch temp
Your branch is up to date with 'origin/temp'.

nothing to commit, working tree clean
$git s
On branch temp
Your branch is up to date with 'origin/temp'.

nothing to commit, working tree clean

大丈夫そうですね。
お読みいただきありがとうございました。

・免責事項

当方は、当記事にコンテンツを掲載するにあたって、その内容、機能等について細心の注意を払っておりますが、コンテンツの内容が正確であるかどうか、最新のものであるかどうか、安全なものであるか等について保証をするものではなく、何らの責任を負うものではありません。また、当方は通知することなく当記事に掲載した情報の訂正、修正、追加、中断、削除等をいつでも行うことができるものとします。また、当記事、またはコンテンツのご利用により、万一、ご利用者様に何らかの不都合や損害が発生したとしても、当方は何らの責任を負うものではありません。