Catalog
flutter/dart-use-path-package

flutter

dart-use-path-package

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.

NewUpdated Sep 26, 2026

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.

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

Files3
3 files · 4.3 KB

Select a file to preview

Overall Score

88/100

Grade

A

Excellent

Grades are signals, not a certification. Always review a skill yourself before use.

Safety

92

Quality

87

Clarity

89

Completeness

82

Summary

This skill provides comprehensive guidance on cross-platform file path manipulation in Dart using `package:path` and `package:file`. It teaches developers to replace unsafe string operations (interpolation, manual splitting, regex) with safe API calls that handle Windows backslashes, POSIX forward slashes, symlinks, and canonical paths. The skill covers path joining, extension extraction, URI conversion, and Git context switching with concrete idioms and anti-patterns.

Detected Capabilities

code pattern analysisstring refactoring guidancecross-platform path manipulationURI/file-system context conversiontest-doubles (FileSystem mocking)

Trigger Keywords

Phrases that agents use to match this skill to user intent.

path manipulationcross-platform pathsreplace string operationsfile extensionwindows backslashposix conversiongit paths

Use Cases

  • Refactor raw string path operations (`split('/')`, `'$dir/$file'`, `.endsWith('.ext')`) into safe `package:path` calls
  • Convert native file paths to POSIX format for Git repositories, manifests, and archives
  • Extract and manipulate file extensions, including compound extensions (`.tar.gz`, `.js.map`)
  • Write cross-platform code that handles Windows backslashes and POSIX forward slashes transparently
  • Test file-system logic with `package:file` mocks using correct `FileSystem.path` contexts
  • Convert between native file paths and URI objects safely on Windows (avoiding drive letter bugs)

Quality Notes

  • Exceptional clarity and structure: each principle is motivated by a specific Windows/POSIX bug, with side-by-side idiom vs. anti-pattern comparisons
  • Comprehensive edge case coverage: handles dotfiles (`.gitignore`), compound extensions (`.js.map`), symlinks, trailing slashes, relative vs. absolute paths, and URI encoding pitfalls
  • Practical workflow audit checklist at the end helps developers systematically refactor legacy code
  • Includes concrete code examples in main content and references supporting example files (though these are not present in the provided manifest)
  • Domain-specific guidance on Git path contexts demonstrates understanding of real-world integration challenges
  • Multi-level extension handling with explicit warnings about multi-dot stems (`main.dart.wasm` vs. `main.dart.js.map`) shows attention to subtle real-world cases
  • No generic filler: every rule has a clear justification and measurable impact
Model: claude-haiku-4-5-20251001Analyzed: Sep 26, 2026

Reviews

Add this skill to your library to leave a review.

No reviews yet

Be the first to share your experience.

Version History

  1. v1.1

    Content updated

    ✦ AIRecommends Dart list patterns (`case ['foo', ...]`) instead of `p.split(path).first` for path prefix checks; clarifies error handling and adds normalization guidance.

    2026-09-26

    LATEST
  2. v1.0

    2026-09-15

    View This VersionInitial version

Use flutter/dart-use-path-package in your dev environment

Command Palette

Search for a command to run...