Tutorial: Membangun aplikasi baris perintah dengan System.CommandLine

Petunjuk / Saran

Artikel ini adalah bagian dari bagian Dasar-Dasar , ditulis untuk pengembang yang mengetahui setidaknya satu bahasa pemrograman dan mempelajari C#. Jika Anda baru dalam pemrograman, mulailah dengan Memulai. Jika Anda memerlukan cakupan pustaka yang komprehensif, lihat dokumentasi pustaka System.CommandLine.

Pustaka System.CommandLine menangani penguraian baris perintah, pembuatan teks bantuan, dan validasi input sehingga Anda dapat fokus pada logika aplikasi Anda. Dalam tutorial ini, Anda membangun CLI pelacak tugas yang menunjukkan konsep inti: perintah, sub perintah, opsi, dan argumen.

Di tutorial ini, Anda akan:

  • Buat aplikasi berbasis file dengan menggunakan System.CommandLine paket .
  • Tentukan opsi dan argumen dengan nilai yang di ketik.
  • Buat sub perintah dan lampirkan opsi dan argumen ke dalamnya.
  • Tangani setiap perintah sub dengan tindakan.
  • Uji aplikasi dengan input baris perintah yang berbeda.

Prasyarat

Membuat aplikasi

Mulailah dengan membuat program C# berbasis file dan menambahkan System.CommandLine paket.

  1. Buat file bernama TaskCli.cs dengan konten berikut:

    #!/usr/bin/env dotnet
    

    Baris #! (shebang) memungkinkan Anda menjalankan file langsung pada sistem Unix. Di Windows, jalankan file dengan dotnet run TaskCli.cs.

  2. Tambahkan direktif paket System.CommandLine dan using pernyataan yang diperlukan.

    #:package System.CommandLine@2.0.0
    
    using System.CommandLine;
    using System.CommandLine.Parsing;
    using System.Text.Json;
    

    Penting

    Versi 2.0.0 adalah versi terbaru pada saat penulisan. Periksa halaman NuGet paket untuk versi terbaru untuk memastikan Anda memiliki perbaikan keamanan terbaru.

Memahami struktur perintah

Sebelum menulis kode penguraian apa pun, pertimbangkan seperti apa tampilan CLI dari perspektif pengguna. Pelacak tugas mendukung empat operasi:

dotnet TaskCli.cs -- add "Write documentation" --priority High --due 2026-04-01
dotnet TaskCli.cs -- list --all
dotnet TaskCli.cs -- complete 3
dotnet TaskCli.cs -- remove 3
dotnet TaskCli.cs -- --verbose list

Nota

Setelah --TaskCli.cs dalam contoh sebelumnya menunjukkan kepada dotnet run bahwa semua argumen yang tersisa diteruskan ke aplikasi Anda, daripada ditafsirkan oleh CLI dotnet itu sendiri.

Setiap baris menggunakan beberapa konsep baris perintah:

  • Subperintah adalah kata kerja untuk memberi tahu aplikasi apa yang harus dilakukan. Pelacak tugas memiliki empat: add, , list, completedan remove. Setiap sub perintah dapat menentukan parameternya sendiri.
  • Argumen adalah nilai posisi yang mengikuti sub-perintah. Dalam add "Write documentation", string "Write documentation" adalah argumen yang menentukan deskripsi tugas. Dalam complete 3, angka 3 adalah argumen yang menentukan ID tugas.
  • Opsi diberi nama nilai yang diawali dengan --. Dalam add --priority High --due 2026-04-01, keduanya --priority dan --due merupakan opsi dengan nilainya sendiri. Di list --all, --all opsinya adalah bendera Boolean yang tidak memerlukan nilai.
  • Opsi global berlaku untuk setiap sub-perintah. Opsi --verbose didefinisikan pada perintah root dengan Recursive = true, sehingga berfungsi dengan sub-perintah apa pun. Dalam --verbose list, bendera verbose muncul sebelum sub-perintah, tetapi list --verbose berfungsi dengan sama baiknya.

Di bagian berikut, Anda membangun potongan-potongan ini dari bawah ke atas. Pertama, Anda menentukan opsi individual (seperti --priority dan --all) dan argumen (seperti deskripsi tugas dan ID). Selanjutnya, Anda membuat empat sub-perintah dan melampirkan opsi dan argumen yang relevan ke masing-masing. Kemudian Anda menyambungkan tindakan untuk setiap subperintah. Tindakan adalah kode yang berjalan saat pengguna memanggil perintah tersebut. Terakhir, Anda merakit perintah akar, mengurai input, dan memanggil tindakan yang cocok.

Untuk melihat lebih dalam konsep sintaks baris perintah, lihat Gambaran umum sintaks baris perintah.

Menentukan opsi dan argumen

Opsi mewakili nilai bernama yang ditentukan pengguna dengan -- awalan. Argumen mewakili nilai posisi. Keduanya memiliki tipe data yang kuat. System.CommandLine mengurai string input ke dalam jenis yang Anda tentukan.

Pustaka System.CommandLine menggunakan jenis generik untuk menjamin keamanan jenis. Saat Anda menulis Option<int>, yang terletak di antara int tanda kurung sudut adalah argumen jenis. Ini beritahu pustaka jenis nilai apa yang dimiliki opsi. Kelas itu sendiri mendeklarasikan parameter tipe (seperti dalam System.CommandLine.Option<T>), dan Anda menyediakan tipe konkret ketika Anda membuat sebuah instans. Pustaka mengurai string input pengguna dan mengonversinya ke jenis tersebut secara otomatis. Jika pengguna menyediakan --delay abcOption<int>, System.CommandLine melaporkan kesalahan penguraian alih-alih meneruskan data buruk ke kode Anda. Anda akan melihat pola ini dengan Option<bool>, , Option<Priority>Option<DateOnly?>, dan System.CommandLine.Argument<T> dalam langkah-langkah berikut. Untuk informasi selengkapnya tentang generik, lihat Generik.

  1. Tentukan opsi. Masing-masing Option<T> menentukan jenis nilai, nama, dan deskripsi. Opsi --priority menggunakan enum jenis, dan System.CommandLine secara otomatis memvalidasi input terhadap nilai enum yang valid:

    var verboseOption = new Option<bool>("--verbose")
    {
        Description = "Show detailed output",
        Recursive = true
    };
    
    var priorityOption = new Option<Priority>("--priority")
    {
        Description = "Task priority level",
        DefaultValueFactory = _ => Priority.Medium
    };
    
    var dueOption = new Option<DateOnly?>("--due")
    {
        Description = "Due date (uses current culture date format)"
    };
    
    var allOption = new Option<bool>("--all")
    {
        Description = "Include completed tasks"
    };
    

    Recursive = true Pengaturan pada --verbose membuat opsi tersedia di setiap sub-perintah. DefaultValueFactory pada --priority menyediakan nilai default sehingga pengguna dapat menghilangkan opsi.

  2. Tentukan argumen. Masing-masing Argument<T> menentukan jenis nilai dan nama:

    var descriptionArgument = new Argument<string>("description")
    {
        Description = "Task description"
    };
    
    var taskIdArgument = new Argument<int>("id")
    {
        Description = "Task ID"
    };
    

Perintah build dan sub-perintah

Sebuah System.CommandLine.Command merupakan tindakan yang dapat diaktifkan oleh pengguna. Tambahkan opsi dan argumen yang relevan ke setiap perintah sehingga System.CommandLine mengetahui parameter mana yang termasuk di mana.

  1. Buat empat sub-perintah. Setiap perintah mendapatkan kombinasi opsi dan argumennya sendiri:

    var addCommand = new Command("add", "Add a new task")
    {
        Arguments = { descriptionArgument },
        Options = { priorityOption, dueOption }
    };
    
    var listCommand = new Command("list", "List all tasks")
    {
        Options = { allOption }
    };
    
    var completeCommand = new Command("complete", "Mark a task as complete")
    {
        Arguments = { taskIdArgument }
    };
    
    var removeCommand = new Command("remove", "Remove a task")
    {
        Arguments = { taskIdArgument }
    };
    
  2. Merakit perintah root. System.CommandLine.RootCommand adalah titik masuk untuk CLI. Tambahkan opsi global --verbose dan semua sub-perintah:

    var rootCommand = new RootCommand("A simple task tracker CLI")
    {
        Options = { verboseOption },
        Subcommands = { addCommand, listCommand, completeCommand, removeCommand }
    };
    

    Teks bantuan yang dibuat secara otomatis menunjukkan deskripsi perintah akar saat pengguna menjalankan TaskCli --help.

Mengelola perintah dengan tindakan

Setiap subperindakan memerlukan tindakan. Tindakan adalah delegat yang berjalan saat pengguna memanggil perintah tersebut. Delegasi adalah jenis yang mewakili referensi ke metode. Di sini, Anda meneruskan ekspresi lambda (fungsi anonim sebaris yang didefinisikan dengan =>) sebagai delegate. Panggil SetAction untuk menetapkan setiap tindakan. Delegasi menerima ParseResult yang menyediakan akses ke nilai yang diurai melalui GetValue.

  1. Atur tindakan untuk perintah add. Tindakan ini memperkenalkan interpolasi string ($"..." string yang menyematkan ekspresi dalam kurung kurawal), operator kondisional (?:), dan pola pengujian jenis (due is DateOnly dueDate) yang memeriksa apakah nilai nullable memiliki nilai dan menetapkannya ke variabel baru dalam satu langkah:

    addCommand.SetAction(parseResult =>
    {
        var description = parseResult.GetValue(descriptionArgument)!;
        var priority = parseResult.GetValue(priorityOption);
        var due = parseResult.GetValue(dueOption);
        var verbose = parseResult.GetValue(verboseOption);
    
        var tasks = LoadTasks();
        var id = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
        var task = new TaskItem(id, description, priority, due, false);
        tasks.Add(task);
        SaveTasks(tasks);
    
        Console.WriteLine($"Added task {id}: {description}");
        if (verbose)
        {
            Console.WriteLine($"  Priority: {priority}");
            if (due is DateOnly dueDate)
            {
                Console.WriteLine($"  Due: {dueDate}");
            }
        }
    });
    
  2. Atur tindakan untuk list perintah . LINQ (Kueri Terintegrasi Bahasa) memberi Anda operator kueri standar untuk koleksi dalam memori. Dalam tindakan ini, Enumerable.Where memfilter tugas hanya ke item yang cocok dengan kondisi, dan Enumerable.ToList mewujudkan urutan yang difilter ke dalam daftar. Aksi tersebut kemudian menggunakan foreach perulangan untuk mengiterasi hasil, dan operator bersyarat untuk memilih simbol status:

    listCommand.SetAction(parseResult =>
    {
        var showAll = parseResult.GetValue(allOption);
        var verbose = parseResult.GetValue(verboseOption);
    
        var tasks = LoadTasks();
        var filtered = showAll ? tasks : tasks.Where(t => !t.IsComplete).ToList();
    
        if (filtered.Count == 0)
        {
            Console.WriteLine("No tasks found.");
            return;
        }
    
        foreach (var task in filtered)
        {
            var status = task.IsComplete ? "✓" : " ";
            Console.WriteLine($"  [{status}] {task.Id}: {task.Description}");
            if (verbose)
            {
                Console.WriteLine($"       Priority: {task.Priority}");
                if (task.Due is DateOnly dueDate)
                {
                    Console.WriteLine($"       Due: {dueDate}");
                }
            }
        }
    });
    

    Untuk detail selengkapnya, lihat LINQ.

  3. Atur tindakan untuk complete perintah . Tindakan ini menggunakan LINQ Enumerable.FirstOrDefault untuk menemukan:

    • Tugas mencocokkan
    • Polais null untuk memeriksa apakah tugas ada.
    • Ekspresiwith untuk membuat instans rekaman baru dengan menyalin nilai yang ada terlebih dahulu, lalu menerapkan properti yang Anda tetapkan di with penginisialisasi (di sini, IsComplete = true). Rekaman tidak dapat diubah secara default, sehingga pola salin dan perbarui ini adalah cara Anda menghasilkan nilai yang dimodifikasi.

    Karena tindakan dapat gagal (misalnya, ID tugas tidak ada), tindakan mengembalikan kode kesalahan bilangan bulat yang menjadi kode keluar aplikasi:

    completeCommand.SetAction(parseResult =>
    {
        var id = parseResult.GetValue(taskIdArgument);
        var verbose = parseResult.GetValue(verboseOption);
    
        var tasks = LoadTasks();
        var task = tasks.FirstOrDefault(t => t.Id == id);
    
        if (task is null)
        {
            Console.Error.WriteLine($"Task {id} not found.");
            return -1;
        }
    
        tasks[tasks.IndexOf(task)] = task with { IsComplete = true };
        SaveTasks(tasks);
    
        Console.WriteLine($"Completed task {id}: {task.Description}");
        if (verbose)
        {
            Console.WriteLine($"  Priority: {task.Priority}");
        }
        return 0;
    });
    
  4. Atur tindakan untuk remove perintah . Tindakan ini mengikuti pola pencarian dan validasi yang sama dengan complete. Gunakan FirstOrDefault untuk menemukan tugas dan is null untuk menangani kasus yang hilang:

    removeCommand.SetAction(parseResult =>
    {
        var id = parseResult.GetValue(taskIdArgument);
        var verbose = parseResult.GetValue(verboseOption);
    
        var tasks = LoadTasks();
        var task = tasks.FirstOrDefault(t => t.Id == id);
    
        if (task is null)
        {
            Console.Error.WriteLine($"Task {id} not found.");
            return -1;
        }
    
        tasks.Remove(task);
        SaveTasks(tasks);
    
        Console.WriteLine($"Removed task {id}: {task.Description}");
        if (verbose)
        {
            Console.WriteLine($"  Priority: {task.Priority}");
        }
        return 0;
    });
    
  5. Uraikan baris perintah dan panggil tindakan yang cocok:

    return rootCommand.Parse(args).Invoke();
    

    rootCommand.Parse(args) menguraikan input ke dalam ParseResult, dan .Invoke() menjalankan tindakan untuk perintah yang cocok. Nilai yang dikembalikan adalah kode keluar (0 untuk keberhasilan).

Menambahkan jenis pendukung dan pembantu data

Aplikasi ini membutuhkan beberapa bagian pendukung: fungsi lokal, enum, , recorddan konteks serialisasi. Bagian berikut memperkenalkan setiap konsep, menjelaskan mengapa Anda memilihnya, dan menampilkan kode. Aplikasi ini menggunakan System.Text.Json untuk menyimpan tugas sebagai JSON (JavaScript Object Notation). Aplikasi berbasis file mengharuskan deklarasi jenis muncul setelah semua pernyataan tingkat atas dan fungsi lokal.

  1. Tambahkan fungsi lokal yang memuat dan menyimpan tugas. Fungsi lokal adalah metode yang dideklarasikan di dalam fungsi lain, termasuk di dalam fungsi lokal lainnya. Fungsi lokal menjaga logika pembantu tetap dekat dengan kode yang memanggilnya, yang meningkatkan keterbacaan karena pembaca tidak perlu melompat ke kelas atau file terpisah untuk memahami alur. Di sini, LoadTasks dan SaveTasks merangkum I/O file yang dibagikan beberapa tindakan perintah, sehingga aplikasi menulis logika muat/simpan sekali dan menggunakannya kembali:

    static string GetTaskFilePath() => Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "taskcli-sample",
        "tasks.json");
    
    static List<TaskItem> LoadTasks()
    {
        var path = GetTaskFilePath();
        if (!File.Exists(path))
        {
            return [];
        }
    
        var json = File.ReadAllText(path);
        return JsonSerializer.Deserialize(json, TaskJsonContext.Default.ListTaskItem) ?? [];
    }
    
    static void SaveTasks(List<TaskItem> tasks)
    {
        var path = GetTaskFilePath();
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        var json = JsonSerializer.Serialize(tasks, TaskJsonContext.Default.ListTaskItem);
        File.WriteAllText(path, json);
    }
    
  2. Tambahkan enum Priority dan rekor TaskItem di akhir file.

    public enum Priority { Low, Medium, High }
    

    enum adalah jenis nilai yang menentukan sekumpulan konstanta bernama tetap yang didukung oleh jenis integral. Anda dapat mewakili tingkat prioritas dengan bilangan bulat biasa (0, 1, 2), tetapi merupakan pilihan yang enum lebih baik karena beberapa alasan: pengompilasi membatasi penugasan ke nama yang ditentukan, sehingga kesalahan ketik seperti Hihg menyebabkan kesalahan waktu kompilasi alih-alih bug senyap; nama Low, Medium, dan High membuat kode lebih mudah dibaca; dan System.CommandLine secara otomatis memvalidasi input pengguna terhadap anggota enum, jadi Anda mendapatkan pemeriksaan input gratis.

    public record TaskItem(int Id, string Description, Priority Priority, DateOnly? Due, bool IsComplete);
    

    record adalah jenis yang dilengkapi kompilator dengan kesetaraan berbasis nilai dan mutasi nondestruktif. record adalah pilihan yang tepat untuk TaskItem karena data tugas dalam keadaan sederhana tanpa perilaku kompleks—Anda membandingkan tugas berdasarkan nilainya, bukan berdasarkan identitas referensinya. Pengompilasi menghasilkan dukungan ekspresi Equals, GetHashCode, ToString, dan with dari parameter konstruktor, sehingga Anda mendapatkan pemeriksaan kesetaraan yang tepat, tampilan debugging yang mudah, dan pembaruan tak berubah tanpa harus menulis kode yang berulang. Karena rekaman tidak dapat diubah secara default, Anda menggunakan ekspresi with untuk menghasilkan salinan yang dimodifikasi (seperti aksi complete) daripada memodifikasi yang asli, yang mencegah efek samping tidak sengaja saat aksi yang berbeda membaca dan menulis pada daftar yang sama.

  3. Tambahkan konteks serialisasi JSON untuk serialisasi yang kompatibel dengan AOT:

    [System.Text.Json.Serialization.JsonSourceGenerationOptions(WriteIndented = true)]
    [System.Text.Json.Serialization.JsonSerializable(typeof(List<TaskItem>))]
    internal partial class TaskJsonContext : System.Text.Json.Serialization.JsonSerializerContext;
    

    Kelas ini menggunakan dua atribut (anotasi metadata yang ditempatkan dalam kurung siku di atas deklarasi). Atribut [JsonSourceGenerationOptions(WriteIndented = true)] memberi tahu generator sumber untuk memancarkan JSON yang terindentasi untuk keterbacaan. Atribut [JsonSerializable(typeof(List<TaskItem>))] memberi tahu generator jenis mana yang akan dibuat kode serialisasinya. Bersama-sama, atribut ini memungkinkan serialisasi yang dihasilkan sumber, yang menghindari refleksi run-time dan mendukung kompilasi ahead-of-time (AOT).

Uji aplikasi

Jalankan aplikasi dengan input yang berbeda untuk menjalankan setiap sub-perintah.

  1. Lihat bantuan yang dibuat secara otomatis:

    dotnet run TaskCli.cs -- --help
    
    Description:
      A simple task tracker CLI
    
    Usage:
      TaskCli [command] [options]
    
    Options:
      --verbose       Show detailed output
      -?, -h, --help  Show help and usage information
      --version       Show version information
    
    Commands:
      add <description>  Add a new task
      list               List all tasks
      complete <id>      Mark a task as complete
      remove <id>        Remove a task
    
  2. Tambahkan tugas dengan opsi yang berbeda:

    dotnet run TaskCli.cs -- add "Write documentation" --priority High --due 2026-04-01
    dotnet run TaskCli.cs -- add "Review pull request"
    dotnet run TaskCli.cs -- add "Fix build errors"
    
    Added task 1: Write documentation
    Added task 2: Review pull request
    Added task 3: Fix build errors
    
  3. Cantumkan tugas dan gunakan --verbose untuk menampilkan detail tambahan:

    dotnet run TaskCli.cs -- --verbose list
    
      [ ] 1: Write documentation
           Priority: High
           Due: 4/1/2026
      [ ] 2: Review pull request
           Priority: Medium
      [ ] 3: Fix build errors
           Priority: Medium
    
  4. Selesaikan tugas, lalu verifikasi daftar filter tugas yang diselesaikan secara default:

    dotnet run TaskCli.cs -- complete 2
    dotnet run TaskCli.cs -- list
    
    Completed task 2: Review pull request
      [ ] 1: Write documentation
      [ ] 3: Fix build errors
    
  5. Gunakan --all untuk menyertakan tugas yang telah selesai:

    dotnet run TaskCli.cs -- list --all
    
      [ ] 1: Write documentation
      [✓] 2: Review pull request
      [ ] 3: Fix build errors
    
  6. Hapus tugas:

    dotnet run TaskCli.cs -- remove 3
    
    Removed task 3: Fix build errors
    

Membersihkan sumber daya

Pelacak tugas menyimpan data dalam file JSON di bawah folder data aplikasi lokal Anda. taskcli-sample Hapus folder untuk menghapus data sampel:

  • Windows: Hapus %LOCALAPPDATA%\taskcli-sample.
  • macOS: Hapus ~/Library/Application Support/taskcli-sample.
  • Linux: Hapus ~/.local/share/taskcli-sample.