rksoftware

Visual Studio とか C# とかが好きです

PowerShell で .NET のコンソールアプリにダブルクォーテーションを引数で渡す

PowerShell で .NET のコンソールアプリにダブルクォーテーションを引数で渡す。意外と難しいです。

■ 結論

.\ConsoleApp1.exe \`"

です。

■ 内容

" は PowerShell で引数として書く場合に ` によるエスケープが必要です。その後、.NET のコンソールアプリでも \ によるエスケープが必要です。ということだと思います。

例えば PowerShell だけで動かすとこんな感じです。

 echo \`"
\"

つまり、\`" と書くことで、.NET のコンソールアプリに \" がわたり、アプリ内で " として入ってくるみたいです。

30分くらい悩みました。

.NET のライブラリ作成時のターゲットフレームワークについて

■ 結論1

こんな感じでしょうか。
.NET Framework 派にも使ってもらいたい場合。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>netstandard2.0</TargetFrameworks>
    <LangVersion>12.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

■ 結論2

こんな感じでしょうか。
.NET Framework は見捨てる場合。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>net6.0</TargetFrameworks>
    <LangVersion>12.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

■ ポイント

<LangVersion>12.0</LangVersion>

で、C# の言語バージョンを指定している点です。これを指定することで、ターゲットフレームワークを過去の古いものを指定しても今の C# でコードをかけます。

この設定は、Visual Studio の UI からできるのかよくわからないので、.csproj ファイルを直接編集しています。というか、ターゲットフレームワークも直接編集しています。

言語バージョンをし愛知しない場合

こんな感じのエラーになります。つらい......

CS8936   機能 'コレクション式' は C# 10.0 では使用できません。言語バージョン 12.0 以上を使用してください。

■ ターゲットフレームワークは変えないといけないの?

ターゲットフレームワークを古い過去のものにしておかないと、使ってもらいにくくなります。
過去に生まれた古いプロダクトは過去の古いフレームワークが指定されていることでしょう。例えばライブラリを .NET8、プロダクトが .NET6 の場合は次のようにエラーになります。

エラー (アクティブ)      プロジェクト '..\ClassLibrary2\ClassLibrary2.csproj' は 'net8.0' を対象にしています。'.NETCoreApp,Version=v6.0' を対象とするプロジェクトからは参照できません。

■ TargetFrameworks って何? TargetFramework じゃないの?

TargetFrameworks にしておくと、複数指定が可能になります。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0;net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

こんな感じに。TargetFramework だとエラーになります。

エラー (アクティブ)  NETSDK1046  TargetFramework 値 'net9.0;net8.0' が無効です。複数をターゲットとするには、代わりに 'TargetFrameworks' プロパティを使用してください。

■ ターゲットフレームワークは何を指定しよう

サポートされている .NET は対象にしたほうが良いと思います。
dotnet.microsoft.com
2024/05 現在 .NET6 がサポート対象なので、.NET6 がいいと思います。

もう一つ、.NET Framework で使ってもらいたい場合もあると思います。
正直、言っていてそういう場合はもっと古い .NET Framework なのではという気もしますが、.NET Standard 2.0 でしょう。最新は .NET Stadard 2.1 ですが、.NET Framework が対応していないようです。
learn.microsoft.com

■ 余談

ターゲットフレームワークを変更した場合は、ソリューションを閉じて開きなおしたほうがよさそうです。合っているのにエラーになって解消できなくなったりしました。

衝撃の事実! IT エンジニアは風呂に入る!

IT エンジニアは風呂に入らないと言われています。しかし実際にはどうなのでしょうか?

なんと、8 割の IT エンジニアが実際には風呂に入るという結果に。
IT エンジニアは風呂に入ります。按針ですね。

■ いかがでしたか?

いかがでしたか?

C# で CSV を読み書き (ライブラリ使わず)

以前に C# でライブラリを使って CSV を読み込む方法を試しました。
rksoftware.hatenablog.com

しかし少々コードが長くなってしまったので、手軽に使えるスニペットを書いてみました。CSV の読み書き、何度かいても忘れてしまうのでもう書き残しておくことにします。
こんな感じのコードをプロジェクト内においておけば、しゅっと CSV が扱えるかもしれません。品質は保証できませんけれど。

■ 使い方

var csvString = "a,b,c\n1,2,3\n4,5,6\n";
var data = CsvParser.CsvReader.Read(csvString);
var csvString = "a,b,c\n1,2,3\n4,5,6\n";
var (header, data) = CsvParser.CsvReader.ReadHeaderAndData(csvString);

■ CSV 読み込みコード

public static class CsvReader
{
    public static string[][] Read(string csvString) => _Read(csvString);

    public static (string[] header, string[][] data) ReadHeaderAndData(string csvString)
    {
        var values = Read(csvString);
        return (values.FirstOrDefault() ?? Array.Empty<string>(), values.Skip(1).ToArray());
    }

    private static string[][] _Read(string csvString)
    {
        List<List<List<char>>> data = new();
        (bool isLineHead, bool isQuoted, char quote, bool isEscaped) = (true, false, '"', false);
        foreach (var c in csvString)
        {
            if (isLineHead) { data.Add(new List<List<char>>()); data.LastOrDefault()?.Add(new List<char>()); isLineHead = false; }
            if (data.LastOrDefault()?.LastOrDefault()?.Count == 0 && !isQuoted && (c == '"' || c == '\''))
            {
                (isQuoted, quote, isEscaped) = (true, c, false);
                continue;
            }
            if (!isEscaped && isQuoted && c == quote) { isEscaped = true; continue; }
            if (c == ',' && (!isQuoted || isEscaped)) { data.LastOrDefault()?.Add(new List<char>()); (isQuoted, isEscaped) = (false, false); continue; }
            if (c == '\n' && (!isQuoted || isEscaped)) { isLineHead = true; (isQuoted, isEscaped) = (false, false); continue; }
            isEscaped = false;
            data.LastOrDefault()?.LastOrDefault()?.Add(c);
        }
        return data.Select(line => line.Select(cell => new string(cell.ToArray())).ToArray()).ToArray();
    }
}

■ CSV 文字列化コード

public static class CsvWriter
{
    public static string Write(string[][] data) => _Write(data);

    public static string Write(string[] header, string[][] data) => Write(new string[][] { header }.Concat(data).ToArray());

    private static string _Write(string[][] data)
    {
        var sb = new System.Text.StringBuilder();
        foreach (var line in data ?? Array.Empty<string[]>())
            sb.Append(string.Join(',', line.Select(Escape)) + '\n');
        return sb.ToString();

        static string Escape(string cell) =>
            (cell.Contains('"') || cell.Contains('\'') || cell.Contains(',') || cell.Contains('\n'))
            ? $"\"{cell.Replace("\"", "\"\"")}\""
            : cell;
    }
}

■ GitHub

このコードは GitHub においています。

github.com

C# で CSV 文字列を CSV としてパースして指定のセルでソートして出力するコード

意外と難しかったのでメモ。

CsvHelper というライブラリを使っています。

導入方法

dotnet add package CsvHelper

C# で CSV 文字列を CSV としてパースして指定のセルでソートして出力するコードです。内部的にはデータを配列で扱っています。CSV の中身には決まりがないのでデータオブジェクトにマッピングしない、というスタンスでやってみました。

■ コード

// データの読み込みと加工
var (header, data) = ReadAndOrder(args.FirstOrDefault() ?? string.Empty, args.Skip(1).FirstOrDefault() ?? string.Empty);

// 結果の出力 ヘッダー行とデータ行
Console.WriteLine(string.Join(",", header.Select(Escape)));
Console.WriteLine(string.Join("\n", data.Select(line => string.Join(",", line.Select(Escape)))));


////
// == 以降メソッド ==
////

// データの読み込みと加工メソッド
static (string[], string[][]) ReadAndOrder(string csv, string orderCellName)
{
    // データの読み込みとヘッダー行の抽出
    var headerAndData = Read(csv).ToArray();
    var header = headerAndData.FirstOrDefault() ?? Array.Empty<string>();

    // ソートする列のインデックスを産出
    var orderCellIndex = header.Index().Where(x => string.Compare(x.Item, orderCellName) == 0).Select(x => x.Index + 1).FirstOrDefault() - 1;

    // ソートしつつデータ部の抽出
    var data = headerAndData.Skip(1).OrderBy(x => (-1 < orderCellIndex && orderCellIndex < x.Length) ? x[orderCellIndex] : "").ToArray();

    return (header, data);
}

// 読み込みメソッド CsvHelper 使用
static IEnumerable<string[]> Read(string csv)
{
    using var sr = new StringReader(csv);
    using var helper = new CsvHelper.CsvReader(sr, System.Globalization.CultureInfo.CurrentCulture);
    while (helper.Read())
        yield return Enumerable.Range(0, helper.Parser.Count).Select(i => helper.Parser[i]).ToArray();
}

// 結果出力時のエスケープ
static string Escape(string cell) =>
    (cell.Contains('"') || cell.Contains('\'') || cell.Contains(',') || cell.Contains('\n'))
    ? $"\"{cell.Replace("\"", "\"\"")}\""
    : cell;

■ 実行例

h1 列でソート

.\kaito.exe h1,h2`n3,6`n5,4`n6,5`n4,3 h1
h1,h2
3,6
4,3
5,4
6,5

h2 列でソート

.\kaito.exe h1,h2`n3,6`n5,4`n6,5`n4,3 h2
h1,h2
4,3
5,4
6,5
3,6

■ C# Tokyo お題

このコードは C# Tokyo のお題のコードを書いてみよう企画で書いたコードです。

github.com

C# で CSV を読み込む

結構面倒です。

すばらしいライブラリがあるので、ライブラリを使えばいいのですがちょっと重たい気がしたのでメモ。

■ ここでいう重たいとは

インスタンスを作って、動作の設定をセットして、while でわましながら読んだり。セルの読み出しが手間だったりをいうことをイメージしています。 static メソッドに string を渡して、string の二次元配列が返ってくるくらいのシンプルさが欲しい時もあります。というか結構そういうときのほうが多いかもしれないです。

■ どうして

ライブラリはお行儀がいいので、メモリに文字列をため込まない = string でなく stream で受け取る、一気に読まず while で回す、配列で返さずデータクラスのプロパティにマッピングする。いろいろとそういう感じだと思います。

しかし我々はお行儀が悪いので。

ライブラリがお行儀よくでも使う我々はお行儀が悪いので、配列でもらって加工したい、そもそも読むファイルが全行のセルが同じ数だと思うなよ! そんな感じです。

■ ライブラリを見てみよう!

今回見るライブラリは 3 つ。

  • Csv で検索すると出てきてダウンロード数が結構ある Csv
  • CsvHelper 皆大好きなライブラリです。しかし今は以前からは変更が入って使い方がかなり変わっています。データクラス定義しないで使う方法に結構苦しみました。
  • VisualBasic ライブラリの TextFieldParser。古来より受け継がれし遺跡です。.NET Framework 以前の Visula Basic 時代の便利機能が入っているようです。使ったら負けかなと思う由来ですが、正直便利です。TextFieldParser も、正直便利です。
//"a
// b",c""d
// e, f
using Csv;
using CsvHelper.Configuration;

var csv = """
"a
b","c""d"
e,f
""";
Console.WriteLine($"csv ->\n{csv}\n<- csv\n");

{   // Csv というライブラリ
    // dotnet add package Csv
    IEnumerable<Csv.ICsvLine> csvLines = Csv.CsvReader.ReadFromText(csv, new CsvOptions { HeaderMode = HeaderMode.HeaderAbsent });
    Console.WriteLine("Csv ->");
    foreach (Csv.ICsvLine line in csvLines)
        Console.WriteLine($"  row -> {string.Join(" , ", line.Values)}  <- row");
    Console.WriteLine("<- Csv\n");
}

{   // CsvHelper
    // dotnet add package CsvHelper
    using StringReader sr = new StringReader(csv);
    using var helper = new CsvHelper.CsvReader(sr, new CsvConfiguration(System.Globalization.CultureInfo.CurrentCulture) { HasHeaderRecord = false });
    Console.WriteLine("CsvHelper ->");
    while (helper.Read())
    {
        Console.Write($"  row -> ");
        Console.Write(string.Join(" , ", Enumerable.Range(0, helper.Parser.Count).Select(i => helper.Parser[i])));
        Console.WriteLine("  <- row");
    }
    Console.WriteLine("<- CsvHelper\n");
}

{   // VisualBasic (TextFieldParser)
    // dotnet add package Microsoft.VisualBasic
    using StringReader sr = new StringReader(csv);
    using var tfp = new Microsoft.VisualBasic.FileIO.TextFieldParser(sr);
    tfp.SetDelimiters(",");
    Console.WriteLine("VisualBasic (TextFieldParser) ->");
    while (!tfp.EndOfData)
        Console.WriteLine($"  row -> {string.Join(" , ", tfp.ReadFields() ?? Array.Empty<string>())}  <- row");
    Console.WriteLine("<- VisualBasic (TextFieldParser)");
}

■ 実行結果

csv ->
"a
b","c""d"
e,f
<- csv

Csv ->
  row -> "a  <- row
  row -> b" , c"d  <- row
  row -> e , f  <- row
<- Csv

CsvHelper ->
  row -> a
b , c"d  <- row
  row -> e , f  <- row
<- CsvHelper

VisualBasic (TextFieldParser) ->
  row -> a
b , c"d  <- row
  row -> e , f  <- row
<- VisualBasic (TextFieldParser)
  • Csv はセルの中に改行が許されません。
  • CsvHelper はデータはちゃんと読めていますが、配列でほしい時にちょっとインタフェースから取り方がわかりにくいですね。
  • TextFieldParser はもう少し配列でとりやすいですが、お行儀悪く使うにはやはり重いですね。

ままならないものです。お行委¥魏悪く生きるというのは。

いかがでしたか?

いかがでしたか?