PluginBench
Skill
Pass
Audit score 90

dart-use-path-package

flutter/agent-plugins

Cross-platform file path manipulation for Dart—join, split, and normalize paths safely on Windows and POSIX.

What is dart-use-path-package?

Use `package:path` and `package:file` to handle file and directory paths, extensions, and segments in a cross-platform way. Essential when writing, inspecting, joining, or refactoring file paths, or replacing raw string operations like `.split('/')` or string interpolation. Avoid for HTTP URIs, database queries, or non-path string processing.

  • Join path segments safely across Windows (backslash) and POSIX (forward slash) platforms using `p.join()`
  • Split paths into segments and match them with list patterns instead of substring operations
  • Extract, inspect, and manipulate file extensions and compound extensions (`.tar.gz`) with `p.extension()` and `p.withoutExtension()`
  • Convert between native file paths, POSIX paths, and URIs using `p.toUri()`, `p.fromUri()`, and `p.posix.joinAll()`
  • Normalize and canonicalize paths to resolve `.` and `..` segments and handle symlinks and case-insensitive filesystems
  • Strip location specifiers (`:line-col` suffixes) from editor output before processing paths

How to install dart-use-path-package

npx skills add https://github.com/flutter/agent-plugins --skill dart-use-path-package
Prerequisites
  • Add `package:path` to `pubspec.yaml` dependencies
  • Optionally add `package:file` for mockable filesystem abstractions in tests
Claude Code
Cursor
Windsurf
Cline

How to use dart-use-path-package

  1. 1.Import `package:path` as `p` (e.g., `import 'package:path/path.dart' as p;`)
  2. 2.Replace string interpolation (`'$dir/$file'`) with `p.join(dir, file)` to handle OS-native separators
  3. 3.Use `p.split(path)` to decompose paths into segments, then match with list patterns instead of substring checks
  4. 4.Use `p.extension(path)` and `p.withoutExtension(path)` instead of manual string slicing for extensions
  5. 5.Convert paths to POSIX or URL format with `p.posix.joinAll(p.split(path))` or `p.url.joinAll(p.split(path))`
  6. 6.Extract `:line-col` suffixes from editor output with regex before passing paths to `package:path` functions

Use cases

Good for
  • Joining directory and filename components in cross-platform build tools or file generators without hardcoding `/` or `\`
  • Converting Windows native paths to POSIX format for Git repository metadata, `.gitignore` entries, or archive manifests
  • Extracting and validating file extensions in asset pipelines or file type filters
  • Comparing file paths across symlinks and case-insensitive filesystems using canonicalization
  • Parsing editor error messages or stack traces that include `:line-col` location specifiers
Who it's for
  • Dart and Flutter developers writing cross-platform CLI tools, build systems, or file utilities
  • Backend engineers handling file paths in server-side Dart applications
  • Tool authors generating Git metadata, manifests, or configuration files that require POSIX path format

dart-use-path-package FAQ

Why not just use `.replaceAll('\\', '/')` to convert Windows paths to POSIX?

Direct replacement fails on root drives (`C:\` becomes `C:/` with a colon), mixes OS context with POSIX targets, and is error-prone. Instead, use `p.split()` to decompose into segments, then `p.posix.joinAll()` to rejoin safely.

Should I pass all path segments as separate arguments to `p.join()`?

For cross-platform code, yes—`p.join(home, 'sub', 'file.json')` ensures OS-native separators. For POSIX-only targets, prefer 2-argument boundary joining (`p.join(home, '.local/share/app/bin/config.json')`) to preserve readability and grep-ability while avoiding duplicate-slash bugs.

What's the difference between `p.normalize()` and `p.canonicalize()`?

`p.normalize()` resolves `.` and `..` lexically without touching the filesystem. `p.canonicalize()` also resolves symlinks, standardizes case on case-insensitive filesystems, and checks physical file identity. Use `canonicalize()` when comparing paths across symlinks or case-insensitive systems.

How do I handle editor error messages like `file.dart:42:5`?

Extract the file path before the `:line-col` suffix using regex (`RegExp(r'^(.*?):(\\ d+(?:-\\ d+)?)$')`) to separate the path from location info, then pass only the path to `package:path` functions.

Can I use `package:file` instead of `package:path`?

`package:file` provides a mockable filesystem abstraction (useful for testing) and wraps `package:path` internally. Use `package:file` when you need to mock file I/O in tests; use `package:path` for pure path manipulation without filesystem access.

Full instructions (SKILL.md)

Source of truth, from flutter/agent-plugins.


name: dart-use-path-package description: >- Cross-platform file and directory path manipulation, segment splitting, extension extraction, and context conversion using package:path and package:file. Use when writing, inspecting, joining, splitting, or refactoring file paths, directory names, or extensions, or replacing raw string path operations (.split('/'), '$dir/$file', .endsWith('.ext'), .replaceAll('\\', '/')). Don't use for HTTP network URI routing, database query strings, or non-path string processing. metadata: model: models/gemini-3.1-pro-preview last_modified: Wed, 23 Sep 2026 22:10:00 GMT

Safe Cross-Platform Path Manipulation in Dart

Contents


1. Core Principles & Cross-Platform Rules

Avoid Treating File Paths as Raw Strings

  • Native file paths on Windows use backslashes (\), whereas macOS and Linux use forward slashes (/).
  • String operations like .contains('foo/'), .startsWith('foo/'), or .split('/') silently fail on Windows native paths.
  • String interpolation like '$dir/$file' injects forward slashes on Windows and produces duplicate slashes (//) when $dir ends with a trailing slash.

Rule: Always decompose paths into segments using p.split(path) before inspecting directory hierarchy or segment names, and always join path components using p.join(...).

Pragmatic Boundary Joining vs. Multi-Segment Decomposition (p.join)

  • Cross-Platform Libraries (Windows + POSIX): Pass individual path segments to p.join(dir, 'sub', 'file.json') so package:path inserts OS-native separators (\ on Windows, / on POSIX) between every component.
  • POSIX-Only Tools & Static Subpath Greppability: In codebases exclusively targeting Linux/macOS (or when joining a dynamic base path to a known static subpath), decomposing 5–6 static segments into separate arguments (p.join(home, '.local', 'share', 'app', 'bin', 'config.json')) causes dart format to wrap across 6–8 vertical lines and destroys substring greppability (grep / code_search for .local/share/app/bin).
  • Rule for POSIX Targets: Prefer 2-argument boundary joining (p.join(home, '.local/share/app/bin/config.json')). This prevents duplicate-slash bugs (//) at variable boundaries while preserving single-line readability and exact string searchability.

Normalization vs. Canonicalization (p.normalize vs. p.canonicalize)

  • p.normalize(path) resolves . and .. segments purely lexically without consulting the filesystem or standardizing case.
  • When deduplicating directory paths or comparing physical file identity across symlinks, relative roots, or case-insensitive filesystems, use p.canonicalize(path).

Strip Location Specifiers & Convert URIs Safely

  • Strings formatted as <path>:<line>-<col> or <path>:<line> are not pure file paths. Passing them directly to p.normalize or Uri.parse causes bugs (on Windows, Uri.parse mistakes C: for a URI scheme and :line for a port).
  • Extract the trailing :line-col suffix via regular expression (RegExp(r'^(.*?):(\d+(?:-\d+)?)$')) before passing the file path to package:path.
  • URI Boundary Conversions: When converting between file paths and Uri objects, always use p.toUri(path) and p.fromUri(uri) rather than Uri.parse(path) or manual string concatenation.

2. Recommended package:path Idioms vs. String Anti-Patterns

Path Joining

  • Prefer: p.join(dir, file)
  • Avoid: '$dir/$file' or 'a/$b'
  • Why: String interpolation injects / on Windows and creates duplicate slashes (//) when $dir ends with a trailing separator.

Segment Matching

  • Prefer: p.split(path).contains('foo')
  • Avoid: path.contains('foo/')
  • Why: String matching fails on Windows backslashes (foo\bar) and produces false positives on partial substring names (e.g. barfoo/).

Root and Directory Prefixes

  • Prefer: if (p.split(path) case ['foo', ...]) (or case ['foo', ...final rest] when extracting tail segments), or p.isWithin('foo', path)
  • Avoid: path.startsWith('foo/') or p.split(path).first == 'foo'
  • Why: String prefix matching fails on Windows separators (foo\bar). Calling p.split(path).first throws a StateError on empty lists and requires separate .skip(1) slicing, whereas list patterns safely check non-emptiness, match multi-segment prefixes, and optionally bind ...final rest in a single step. When unnormalized relative prefixes like ./foo/bar may appear, use p.isWithin('foo', path) (or p.split(p.normalize(path))).

File Extensions

  • Prefer: p.extension(path) == '.wasm'
  • Avoid: path.endsWith('.wasm')
  • Why: Substring suffix matching falsely matches directories (foo.wasm/) or non-extension suffixes.

Extension Slicing and Compound Extensions

  • Prefer: p.withoutExtension(path) and p.extension(path, 2)
  • Avoid: path.lastIndexOf('.') and manual substring slicing
  • Why: Manual arithmetic breaks on hidden dotfiles (.gitignore) and compound extensions (.js.map, .tar.gz).

POSIX and URL Path Conversion

  • Prefer: p.posix.joinAll(p.split(path)) or p.url.joinAll(p.split(path))
  • Avoid: path.replaceAll(r'\', '/')
  • Why: Ad-hoc separator replacement fails on root drives and mixes OS context with POSIX or URL targets.

URI Conversion

  • Prefer: p.toUri(path) and p.fromUri(uri)
  • Avoid: Uri.parse(path) and uri.path
  • Why: Direct URI parsing fails on Windows drive letters (C:) and leaks percent-encoding (e.g. %20 for spaces).

Directory Basename Helper

  • Prefer: String canonicalDirName(Directory d) => p.basename(p.normalize(d.absolute.path));
  • Avoid: Repeating p.basename(p.normalize(dir.absolute.path)) inline across files.
  • Why: Centralizes canonical directory naming logic and reduces boilerplate.

3. Bridging Native Paths to POSIX, Git, & URL Contexts

Avoid calling .replaceAll('\\', '/') or .replaceAll(r'\', '/') to convert OS-native paths into POSIX paths (for Git, YAML, archive manifests) or URL segments.

Rule: Split the relative native path using p.split(...), inspect segments with Dart 3 list pattern matching, and join using p.posix.joinAll(...) or p.url.joinAll(...). Always call p.relative(filePath, from: root) first so leading root segments ('/' on POSIX or r'C:\' on Windows) do not interfere with relative prefix patterns:

import 'package:path/path.dart' as p;

String computeWebAssetKey(String filePath, String projectRoot) {
  final relative = p.relative(filePath, from: projectRoot);
  final segments = p.split(relative);
  return switch (segments) {
    ['assets', ...] => p.posix.joinAll(segments),
    _ => p.posix.joinAll(['assets', ...segments]),
  };
}

Git Paths and Repository Metadata

  • Git repository tree objects, .gitignore pattern rules, .gitattributes, and git-tracked symlinks strictly use POSIX forward slashes (/), even on Windows.
  • Inserting native Windows backslashes (\) into .gitignore or git commands causes Git to treat \ as an escape character rather than a directory separator, silently breaking pattern matching.
  • When generating .gitignore entries, repository manifests, or symlink targets programmatically from native file paths, convert the relative native path using p.posix.joinAll(p.split(relativePath)) or p.posix.join(...).

4. Mockable File Systems (package:file vs. Global p.*)

In codebases that use package:file (e.g., CLI applications or services tested with MemoryFileSystem), avoid calling top-level p.* functions on File or Directory paths.

  • Top-level p.* functions bind to the host operating system running the test.
  • If a unit test creates a MemoryFileSystem(style: FileSystemStyle.windows) on a Linux or macOS runner, global p.split(file.path) will split on / instead of \, breaking the test.

Rule: Always use the Context attached to the FileSystem (file.fileSystem.path):

import 'package:file/file.dart';

List<String> listSubdirectoryNames(Directory dir) {
  final pathContext = dir.fileSystem.path;
  return dir
      .listSync()
      .whereType<Directory>()
      .map((d) => pathContext.basename(d.path))
      .toList();
}

5. Extensions, Compound Extensions & Stem Extraction

Avoid manual .lastIndexOf('.') and .substring() arithmetic when extracting file extensions or inserting content hashes. p.extension natively supports multi-level extensions via its optional level parameter.

  • Multi-Dot Stem Nuance: Calling p.extension('main.dart.wasm', 2) returns '.dart.wasm' because it blindly captures the last two dot-separated segments. When hashing or stripping extensions on files that may have multi-dot stems (e.g., main.dart.wasm vs. main.dart.js.map), check whether p.extension(filename, 2) matches a known compound extension (or .endsWith('.map')) before falling back to single-level p.extension(filename):
import 'package:path/path.dart' as p;

String insertContentHash(String filename, String hash) {
  final compoundExt = p.extension(filename, 2);
  // Only use the 2-level extension for true compound suffixes (e.g., '.js.map')
  final ext = compoundExt.endsWith('.map')
      ? compoundExt
      : p.extension(filename);
  final stem = filename.substring(0, filename.length - ext.length);
  return '$stem.$hash$ext';
}

6. Workflows & Audit Checklist

Path Refactoring Checklist

  • Replace string interpolation ('$dir/$file') with p.join(dir, file).
  • Replace .contains('dir/') and .startsWith('dir/') with p.split(path) list pattern checks (case ['dir', ...final rest]) or p.isWithin(parent, child).
  • Replace .replaceAll(r'\', '/') with p.posix.joinAll(p.split(path)) (or p.url.joinAll).
  • Replace .endsWith('.ext') on file paths with p.extension(path) == '.ext'.
  • Replace manual dot-index slicing with p.withoutExtension(path) and p.extension(path, [level]).
  • Verify that code using package:file accesses fileSystem.path instead of global p.*.
  • Ensure Git paths, .gitignore entries, and symlink targets use p.posix forward slashes.

References & Examples