diff --git a/Cargo.lock b/Cargo.lock
index 0476a15eb11..b6f75aad043 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1912,6 +1912,24 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06069a848f95fceae3e5e03c0ddc8cb78452b56654ee0c8e68f938cf790fb9e3"
 
+[[package]]
+name = "xflags"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a6292b9528efc06cb25a41b8a0814dd3a9590c0fe2cd95341fe41bbe034fafb"
+dependencies = [
+ "xflags-macros",
+]
+
+[[package]]
+name = "xflags-macros"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba2108d40e49a0653f2ee4eda59f51447e0cab5cc2cc197a5abd96525c6bd89e"
+dependencies = [
+ "proc-macro2",
+]
+
 [[package]]
 name = "xshell"
 version = "0.1.9"
@@ -1933,11 +1951,11 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "flate2",
- "pico-args",
  "proc-macro2",
  "quote",
  "ungrammar",
  "walkdir",
  "write-json",
+ "xflags",
  "xshell",
 ]
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 0455dd2eb39..b17dde598b0 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -9,11 +9,11 @@ license = "MIT OR Apache-2.0"
 [dependencies]
 anyhow = "1.0.26"
 flate2 = "1.0"
-pico-args = "0.4.0"
 proc-macro2 = "1.0.8"
 quote = "1.0.2"
 ungrammar = "=1.11"
 walkdir = "2.3.1"
 write-json = "0.1.0"
 xshell = "0.1"
+xflags = "0.1.2"
 # Avoid adding more dependencies to this crate
diff --git a/xtask/src/codegen.rs b/xtask/src/codegen.rs
index 743e83e7660..2f56c5ad0c6 100644
--- a/xtask/src/codegen.rs
+++ b/xtask/src/codegen.rs
@@ -18,7 +18,7 @@ use std::{
 };
 use xshell::{cmd, pushenv, read_file, write_file};
 
-use crate::{ensure_rustfmt, project_root, Result};
+use crate::{ensure_rustfmt, flags, project_root, Result};
 
 pub(crate) use self::{
     gen_assists_docs::{generate_assists_docs, generate_assists_tests},
@@ -35,11 +35,7 @@ pub(crate) enum Mode {
     Verify,
 }
 
-pub(crate) struct CodegenCmd {
-    pub(crate) features: bool,
-}
-
-impl CodegenCmd {
+impl flags::Codegen {
     pub(crate) fn run(self) -> Result<()> {
         if self.features {
             generate_lint_completions(Mode::Overwrite)?;
diff --git a/xtask/src/flags.rs b/xtask/src/flags.rs
new file mode 100644
index 00000000000..5710fbdb5d5
--- /dev/null
+++ b/xtask/src/flags.rs
@@ -0,0 +1,139 @@
+#![allow(unreachable_pub)]
+
+xflags::args_parser! {
+    /// Run custom build command.
+    cmd xtask {
+        default cmd help {
+            /// Print help information.
+            optional -h, --help
+        }
+
+        /// Install rust-analyzer server or editor plugin.
+        cmd install {
+            /// Install only VS Code plugin.
+            optional --client
+            /// One of 'code', 'code-exploration', 'code-insiders', 'codium', or 'code-oss'.
+            optional --code-bin name: String
+
+            /// Install only the language server.
+            optional --server
+            /// Use mimalloc allocator for server
+            optional --mimalloc
+            /// Use jemalloc allocator for server
+            optional --jemalloc
+        }
+
+        cmd codegen {
+            optional --features
+        }
+
+        cmd lint {}
+        cmd fuzz-tests {}
+        cmd pre-cache {}
+
+        cmd release {
+            optional --dry-run
+        }
+        cmd promote {
+            optional --dry-run
+        }
+        cmd dist {
+            optional --nightly
+            optional --client version: String
+        }
+        cmd metrics {
+            optional --dry-run
+        }
+        /// Builds a benchmark version of rust-analyzer and puts it into `./target`.
+        cmd bb
+            required suffix: String
+        {}
+    }
+}
+
+// generated start
+// The following code is generated by `xflags` macro.
+// Run `env XFLAGS_DUMP= cargo build` to regenerate.
+#[derive(Debug)]
+pub struct Xtask {
+    pub subcommand: XtaskCmd,
+}
+
+#[derive(Debug)]
+pub enum XtaskCmd {
+    Help(Help),
+    Install(Install),
+    Codegen(Codegen),
+    Lint(Lint),
+    FuzzTests(FuzzTests),
+    PreCache(PreCache),
+    Release(Release),
+    Promote(Promote),
+    Dist(Dist),
+    Metrics(Metrics),
+    Bb(Bb),
+}
+
+#[derive(Debug)]
+pub struct Help {
+    pub help: bool,
+}
+
+#[derive(Debug)]
+pub struct Install {
+    pub client: bool,
+    pub code_bin: Option<String>,
+    pub server: bool,
+    pub mimalloc: bool,
+    pub jemalloc: bool,
+}
+
+#[derive(Debug)]
+pub struct Codegen {
+    pub features: bool,
+}
+
+#[derive(Debug)]
+pub struct Lint {}
+
+#[derive(Debug)]
+pub struct FuzzTests {}
+
+#[derive(Debug)]
+pub struct PreCache {}
+
+#[derive(Debug)]
+pub struct Release {
+    pub dry_run: bool,
+}
+
+#[derive(Debug)]
+pub struct Promote {
+    pub dry_run: bool,
+}
+
+#[derive(Debug)]
+pub struct Dist {
+    pub nightly: bool,
+    pub client: Option<String>,
+}
+
+#[derive(Debug)]
+pub struct Metrics {
+    pub dry_run: bool,
+}
+
+#[derive(Debug)]
+pub struct Bb {
+    pub suffix: String,
+}
+
+impl Xtask {
+    pub const HELP: &'static str = Self::_HELP;
+
+    pub fn from_env() -> xflags::Result<Self> {
+        let mut p = xflags::rt::Parser::new_from_env();
+        Self::_parse(&mut p)
+    }
+}
+// generated end
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 84b17ce2303..e419db7a717 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -7,6 +7,8 @@
 //!
 //! This binary is integrated into the `cargo` command line by using an alias in
 //! `.cargo/config`.
+mod flags;
+
 mod codegen;
 mod ast_src;
 #[cfg(test)]
@@ -19,8 +21,6 @@ mod metrics;
 mod pre_cache;
 
 use anyhow::{bail, Result};
-use codegen::CodegenCmd;
-use pico_args::Arguments;
 use std::{
     env,
     path::{Path, PathBuf},
@@ -32,42 +32,19 @@ use crate::{
     codegen::Mode,
     dist::DistCmd,
     install::{InstallCmd, Malloc, ServerOpt},
-    metrics::MetricsCmd,
-    pre_cache::PreCacheCmd,
-    release::{PromoteCmd, ReleaseCmd},
 };
 
 fn main() -> Result<()> {
     let _d = pushd(project_root())?;
 
-    let mut args = Arguments::from_env();
-    let subcommand = args.subcommand()?.unwrap_or_default();
-
-    match subcommand.as_str() {
-        "install" => {
-            if args.contains(["-h", "--help"]) {
-                eprintln!(
-                    "\
-cargo xtask install
-Install rust-analyzer server or editor plugin.
-
-USAGE:
-    cargo xtask install [FLAGS]
-
-FLAGS:
-        --client[=CLIENT] Install only VS Code plugin.
-                          CLIENT is one of 'code', 'code-exploration', 'code-insiders', 'codium', or 'code-oss'
-        --server          Install only the language server
-        --mimalloc        Use mimalloc allocator for server
-        --jemalloc        Use jemalloc allocator for server
-    -h, --help            Prints help information
-        "
-                );
-                return Ok(());
-            }
-            let server = args.contains("--server");
-            let client_code = args.contains("--client");
-            if server && client_code {
+    let flags = flags::Xtask::from_env()?;
+    match flags.subcommand {
+        flags::XtaskCmd::Help(_) => {
+            println!("{}", flags::Xtask::HELP);
+            return Ok(());
+        }
+        flags::XtaskCmd::Install(flags) => {
+            if flags.server && flags.client {
                 eprintln!(
                     "error: The argument `--server` cannot be used with `--client`\n\n\
                      For more information try --help"
@@ -75,102 +52,43 @@ FLAGS:
                 return Ok(());
             }
 
-            let malloc = if args.contains("--mimalloc") {
+            let malloc = if flags.mimalloc {
                 Malloc::Mimalloc
-            } else if args.contains("--jemalloc") {
+            } else if flags.jemalloc {
                 Malloc::Jemalloc
             } else {
                 Malloc::System
             };
 
-            let client_opt = args.opt_value_from_str("--client")?;
-
-            finish_args(args)?;
+            let client_bin = flags.code_bin.map(|it| it.parse()).transpose()?;
 
             InstallCmd {
-                client: if server { None } else { Some(client_opt.unwrap_or_default()) },
-                server: if client_code { None } else { Some(ServerOpt { malloc }) },
+                client: if flags.server { None } else { client_bin },
+                server: if flags.client { None } else { Some(ServerOpt { malloc }) },
             }
             .run()
         }
-        "codegen" => {
-            let features = args.contains("--features");
-            finish_args(args)?;
-            CodegenCmd { features }.run()
+        flags::XtaskCmd::Codegen(cmd) => cmd.run(),
+        flags::XtaskCmd::Lint(_) => run_clippy(),
+        flags::XtaskCmd::FuzzTests(_) => run_fuzzer(),
+        flags::XtaskCmd::PreCache(cmd) => cmd.run(),
+        flags::XtaskCmd::Release(cmd) => cmd.run(),
+        flags::XtaskCmd::Promote(cmd) => cmd.run(),
+        flags::XtaskCmd::Dist(flags) => {
+            DistCmd { nightly: flags.nightly, client_version: flags.client }.run()
         }
-        "lint" => {
-            finish_args(args)?;
-            run_clippy()
-        }
-        "fuzz-tests" => {
-            finish_args(args)?;
-            run_fuzzer()
-        }
-        "pre-cache" => {
-            finish_args(args)?;
-            PreCacheCmd.run()
-        }
-        "release" => {
-            let dry_run = args.contains("--dry-run");
-            finish_args(args)?;
-            ReleaseCmd { dry_run }.run()
-        }
-        "promote" => {
-            let dry_run = args.contains("--dry-run");
-            finish_args(args)?;
-            PromoteCmd { dry_run }.run()
-        }
-        "dist" => {
-            let nightly = args.contains("--nightly");
-            let client_version: Option<String> = args.opt_value_from_str("--client")?;
-            finish_args(args)?;
-            DistCmd { nightly, client_version }.run()
-        }
-        "metrics" => {
-            let dry_run = args.contains("--dry-run");
-            finish_args(args)?;
-            MetricsCmd { dry_run }.run()
-        }
-        "bb" => {
-            let suffix: String = args.free_from_str()?;
-            finish_args(args)?;
+        flags::XtaskCmd::Metrics(cmd) => cmd.run(),
+        flags::XtaskCmd::Bb(cmd) => {
             {
                 let _d = pushd("./crates/rust-analyzer")?;
                 cmd!("cargo build --release --features jemalloc").run()?;
             }
-            cp("./target/release/rust-analyzer", format!("./target/rust-analyzer-{}", suffix))?;
-            Ok(())
-        }
-        _ => {
-            eprintln!(
-                "\
-cargo xtask
-Run custom build command.
-
-USAGE:
-    cargo xtask <SUBCOMMAND>
-
-SUBCOMMANDS:
-    fuzz-tests
-    codegen
-    install
-    lint
-    dist
-    promote
-    bb"
-            );
+            cp("./target/release/rust-analyzer", format!("./target/rust-analyzer-{}", cmd.suffix))?;
             Ok(())
         }
     }
 }
 
-fn finish_args(args: Arguments) -> Result<()> {
-    if !args.finish().is_empty() {
-        bail!("Unused arguments.");
-    }
-    Ok(())
-}
-
 fn project_root() -> PathBuf {
     Path::new(
         &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()),
diff --git a/xtask/src/metrics.rs b/xtask/src/metrics.rs
index babc2a6d4c5..eb58b327452 100644
--- a/xtask/src/metrics.rs
+++ b/xtask/src/metrics.rs
@@ -9,13 +9,11 @@ use std::{
 use anyhow::{bail, format_err, Result};
 use xshell::{cmd, mkdir_p, pushd, pushenv, read_file, rm_rf};
 
+use crate::flags;
+
 type Unit = String;
 
-pub(crate) struct MetricsCmd {
-    pub(crate) dry_run: bool,
-}
-
-impl MetricsCmd {
+impl flags::Metrics {
     pub(crate) fn run(self) -> Result<()> {
         let mut metrics = Metrics::new()?;
         if !self.dry_run {
diff --git a/xtask/src/pre_cache.rs b/xtask/src/pre_cache.rs
index 54f4a95a99e..b456224fd69 100644
--- a/xtask/src/pre_cache.rs
+++ b/xtask/src/pre_cache.rs
@@ -6,9 +6,9 @@ use std::{
 use anyhow::Result;
 use xshell::rm_rf;
 
-pub(crate) struct PreCacheCmd;
+use crate::flags;
 
-impl PreCacheCmd {
+impl flags::PreCache {
     /// Cleans the `./target` dir after the build such that only
     /// dependencies are cached on CI.
     pub(crate) fn run(self) -> Result<()> {
diff --git a/xtask/src/release.rs b/xtask/src/release.rs
index 5008881e485..d8d86fd63ed 100644
--- a/xtask/src/release.rs
+++ b/xtask/src/release.rs
@@ -2,13 +2,9 @@ use std::fmt::Write;
 
 use xshell::{cmd, cp, pushd, read_dir, write_file};
 
-use crate::{codegen, date_iso, is_release_tag, project_root, Mode, Result};
+use crate::{codegen, date_iso, flags, is_release_tag, project_root, Mode, Result};
 
-pub(crate) struct ReleaseCmd {
-    pub(crate) dry_run: bool,
-}
-
-impl ReleaseCmd {
+impl flags::Release {
     pub(crate) fn run(self) -> Result<()> {
         if !self.dry_run {
             cmd!("git switch release").run()?;
@@ -86,11 +82,7 @@ https://github.com/sponsors/rust-analyzer[GitHub Sponsors].
     }
 }
 
-pub(crate) struct PromoteCmd {
-    pub(crate) dry_run: bool,
-}
-
-impl PromoteCmd {
+impl flags::Promote {
     pub(crate) fn run(self) -> Result<()> {
         let _dir = pushd("../rust-rust-analyzer")?;
         cmd!("git switch master").run()?;