mcpField
D
dart-use-path-package

dart-use-path-package

github.com/flutter开发3.1k6.4k 次安装2 天前更新
代码仓库

关于 dart-use-path-package

面向 Dart 开发者的路径处理技能,介绍如何使用 `package:path` 编写跨平台文件系统代码,并避免字符串操作造成的错误。内容涵盖原生路径、POSIX、URL 和模拟文件系统场景。

主要功能

  1. 01跨平台路径拼接与匹配
  2. 02路径规范化与规范解析
  3. 03安全转换 URI 和 POSIX 路径
  4. 04处理扩展名与复合后缀
  5. 05使用模拟文件系统路径上下文
  6. 06路径重构审查清单

适用场景

  1. 01替换易出错的路径字符串拼接
  2. 02从原生路径生成 Git 路径
  3. 03在文件路径与 URI 间转换
  4. 04在其他主机上测试 Windows 风格路径

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/flutter/agent-plugins --skill dart-use-path-package

安装后在 Claude Code 中输入/dart-use-path-package确认已加载。

附带文件examples/cross_platform_paths.dartexamples/file_system_context.dart

详细介绍

Dart 中安全的跨平台路径操作

目录


1. 核心原则与跨平台规则

避免将文件路径视为原始字符串
  • Windows 上的本机文件路径使用反斜杠(\),而 macOS 和 Linux 使用正斜杠(/)。
  • .contains('foo/')、.startsWith('foo/') 或 .split('/') 这类字符串操作在 Windows 本机路径上会悄然失效。
  • '$dir/$file' 这类字符串插值会在 Windows 上插入正斜杠;当 $dir 以尾随斜杠结尾时,还会产生重复斜杠(//)。

规则:检查目录层级或路径片段名称前,始终使用 p.split(path) 将路径拆分为各个片段;拼接路径组件时,始终使用 p.join(...)。

灵活地进行边界拼接,而非拆分多个片段(p.join)
  • 跨平台库(Windows + POSIX):将单个路径片段传递给 p.join(dir, 'sub', 'file.json'),让 package:path 在每个组件之间插入操作系统本机分隔符(Windows 上为 \,POSIX 上为 /)。
  • 仅面向 POSIX 的工具与静态子路径可搜索性:在仅面向 Linux/macOS 的代码库中(或将动态基路径与已知静态子路径拼接时),把 5–6 个静态片段拆成单独参数(p.join(home, '.local', 'share', 'app', 'bin', 'config.json'))会导致 dart format 将代码换成 6–8 行,破坏子字符串可搜索性(使用 grep / code_search 搜索 .local/share/app/bin)。
  • 面向 POSIX 的规则:优先采用双参数边界拼接(p.join(home, '.local/share/app/bin/config.json'))。这样既能避免变量边界处出现重复斜杠(//)的问题,又能保持单行可读性和精确的字符串搜索能力。
规范化与规范形态化(p.normalize 与 p.canonicalize)
  • p.normalize(path) 仅通过词法方式解析 . 和 .. 片段,不会查询文件系统,也不会统一大小写。
  • 对目录路径去重,或跨符号链接、相对根目录或不区分大小写的文件系统比较物理文件身份时,请使用 p.canonicalize(path)。
安全地去除位置说明符并转换 URI
  • <path>:<line>-<col> 或 <path>:<line> 格式的字符串并非纯文件路径。直接将其传递给 p.normalize 或 Uri.parse 会导致错误(在 Windows 上,Uri.parse 会将 C: 误认为 URI 方案,并将 :line 误认为端口)。
  • 将文件路径传递给 package:path 之前,使用正则表达式(RegExp(r'^(.*?):(\d+(?:-\d+)?)$'))提取末尾的 :line-col 后缀。
  • URI 边界转换:在文件路径与 Uri 对象之间转换时,始终使用 p.toUri(path) 和 p.fromUri(uri),不要使用 Uri.parse(path) 或手动拼接字符串。

2. 推荐的 package:path 惯用写法与字符串反模式

路径拼接
  • 推荐:p.join(dir, file)
  • 避免:'$dir/$file' 或 'a/$b'
  • 原因:字符串插值会在 Windows 上插入 /;当 $dir 以尾随分隔符结尾时,还会产生重复斜杠 (//)。
路径片段匹配
  • 推荐:p.split(path).contains('foo')
  • 避免:path.contains('foo/')
  • 原因:字符串匹配无法处理 Windows 反斜杠(foo\bar),还会对部分子字符串名称产生 误报(例如 barfoo/)。
根目录和目录前缀
  • 优先使用:if (p.split(path) case ['foo', ...])(提取尾部片段时可使用 case ['foo', ...final rest]),或 p.isWithin('foo', path)
  • 避免使用:path.startsWith('foo/') 或 p.split(path).first == 'foo'
  • 原因:字符串前缀匹配无法处理 Windows 分隔符(foo\bar)。调用 p.split(path).first 在列表为空时会抛出 StateError,而且需要单独使用 .skip(1) 切片;列表模式则可以安全地检查非空、匹配多段前缀,并可选择在 一步中绑定 ...final rest。当可能出现 ./foo/bar 这类未规范化的相对前缀时,请使用 p.isWithin('foo', path)(或 p.split(p.normalize(path)))。
文件扩展名
  • 优先使用:p.extension(path) == '.wasm'
  • 避免使用:path.endsWith('.wasm')
  • 原因:子字符串后缀匹配会错误地匹配目录(foo.wasm/)或并非扩展名的后缀。
扩展名切片和复合扩展名
  • 优先使用:p.withoutExtension(path) 和 p.extension(path, 2)
  • 避免使用:path.lastIndexOf('.') 和手动进行 substring 切片
  • 原因:手动计算在隐藏点文件(.gitignore)和复合扩展名(.js.map、.tar.gz)上会出错。
POSIX 和 URL 路径转换
  • 优先使用:p.posix.joinAll(p.split(path)) 或 p.url.joinAll(p.split(path))
  • 避免使用:path.replaceAll(r'\', '/')
  • 原因:临时替换分隔符无法正确处理根驱动器,还会混淆操作系统上下文与 POSIX 或 URL 目标。
目录基名辅助函数
  • 优先使用: String canonicalDirName(Directory d) => p.basename(p.normalize(d.absolute.path));
  • 避免使用:在多个文件中重复内联编写 p.basename(p.normalize(dir.absolute.path))
  • 原因:集中处理规范目录命名逻辑,并减少样板代码。

3. 将原生路径转换为 POSIX、Git 和 URL 上下文

避免调用 .replaceAll('\\', '/') 或 .replaceAll(r'\', '/'),将操作系统原生路径转换为 POSIX 路径(用于 Git、YAML、归档清单)或 URL 片段。

规则:使用 p.split(...) 拆分原生相对路径,使用 Dart 3 列表模式匹配检查各个片段,并使用 p.posix.joinAll(...) 或 p.url.joinAll(...) 拼接。务必先调用 p.relative(filePath, from: root),避免开头的根路径片段(POSIX 上的 '/' 或 Windows 上的 r'C:\')干扰相对前缀模式:

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 路径和仓库元数据
  • Git 仓库树对象、.gitignore 模式规则、.gitattributes 和由 Git 跟踪的符号链接都严格使用 POSIX 正斜杠(/),即使在 Windows 上也一样。
  • 在 .gitignore 或 Git 命令中插入原生 Windows 反斜杠(\),会导致 Git 将 \ 视为转义字符,而非目录分隔符,从而悄然破坏模式匹配。
  • 以编程方式根据原生文件路径生成 .gitignore 条目、仓库清单或符号链接目标时,请使用 p.posix.joinAll(p.split(relativePath)) 或 p.posix.join(...) 转换原生相对路径。

4. 可模拟的文件系统(package:file 与全局 p.*)

在使用 package:file 的代码库中(例如使用 MemoryFileSystem 测试的 CLI 应用或服务),避免对 File 或 Directory 路径调用顶层 p.* 函数。

  • 顶层 p.* 函数会绑定到运行测试的主机操作系统。
  • 如果单元测试在 Linux 或 macOS 运行器上创建了 MemoryFileSystem(style: FileSystemStyle.windows),全局 p.split(file.path) 会按 / 而非 \ 进行拆分,导致测试出错。

规则:始终使用附加到 FileSystem 的 Context(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. 扩展名、复合扩展名与文件名主体提取

提取文件扩展名或插入内容哈希时,避免手动使用 .lastIndexOf('.') 和 .substring() 进行计算。p.extension 原生支持通过可选的 level 参数处理多级扩展名。

  • 多点号文件名主体的细节:调用 p.extension('main.dart.wasm', 2) 会返回 '.dart.wasm',因为它会直接提取最后两个以点分隔的部分。对可能具有多点号文件名主体的文件(例如 main.dart.wasm 和 main.dart.js.map)进行哈希处理或去除扩展名时,请先检查 p.extension(filename, 2) 是否与已知复合扩展名匹配(或使用 .endsWith('.map')),再回退到单级 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. 工作流与审计清单

路径重构清单
  • 将字符串插值('$dir/$file')替换为 p.join(dir, file)。
  • 将 .contains('dir/') 和 .startsWith('dir/') 替换为 p.split(path) 列表模式检查(case ['dir', ...final rest])或 p.isWithin(parent, child)。
  • 将 .replaceAll(r'\', '/') 替换为 p.posix.joinAll(p.split(path))(或 p.url.joinAll)。
  • 将文件路径上的 .endsWith('.ext') 替换为 p.extension(path) == '.ext'。
  • 将手动按点号索引切片替换为 p.withoutExtension(path) 和 p.extension(path, [level])。
  • 确认使用 package:file 的代码通过 fileSystem.path 而非全局 p.* 访问路径。
  • 确保 Git 路径、.gitignore 条目和符号链接目标使用 p.posix 正斜杠。

参考与示例

评论

支持 Markdown