dart学习第 17 节:文件操作与命令行工具​​

92 阅读5分钟

在前几节课中,我们学习了库与包管理,掌握了代码组织和依赖管理的核心技能。今天我们将探索 Dart 在文件操作命令行工具开发方面的能力。这些技能在数据处理、自动化脚本、工具开发等场景中非常实用,能极大提升你的开发效率。

一、文件操作基础:使用 dart:io 库

Dart 标准库中的 dart:io 提供了丰富的文件和目录操作 API,支持读取、写入、删除文件以及目录管理等功能。

1. 引入 dart:io 库

使用文件操作前,需要先导入 dart:io 库:

import 'dart:io';

2. 读取文件内容

读取文件有多种方式,根据文件大小和需求选择合适的方法:

(1)一次性读取小文件

在项目根目录下新建example.txt, 写入任意内容。对于小型文本文件,可以用 readAsString() 一次性读取全部内容:

import 'dart:io';

void main() async {
  // 定义文件路径
  final file = File('example.txt');

  try {
    // 读取文件内容(异步操作)
    String content = await file.readAsString();
    print('文件内容:');
    print(content);
  } catch (e) {
    print('读取文件失败:$e');
  }
}
(2)按行读取文件

在项目根目录下新建large_file.txt, 写入任意内容。对于大型文件,按行读取更高效,避免占用过多内存:

import 'dart:convert';
import 'dart:io';

void main() async {
  final file = File('large_file.txt');

  try {
    // 按行读取(返回 Stream<String>)
    Stream<String> lines = file
        .openRead()
        .transform(utf8.decoder)
        .transform(LineSplitter());

    await for (String line in lines) {
      print('行内容:$line');
      // 可以在这里添加处理逻辑,如查找特定内容
    }
  } catch (e) {
    print('读取文件失败:$e');
  }
}

需要额外导入编码和行分割器:

import 'dart:convert'; // 提供 utf8 编码

3. 写入文件内容

写入文件同样有多种方式,支持覆盖写入或追加内容:

(1)覆盖写入文件
import 'dart:io';

void main() async {
  final file = File('output.txt');

  try {
    // 写入字符串(覆盖原有内容)
    await file.writeAsString('Hello, Dart 文件操作!\n这是第二行内容');
    print('文件写入成功');
  } catch (e) {
    print('文件写入失败:$e');
  }
}
(2)追加内容到文件
import 'dart:io';

void main() async {
  final file = File('output.txt');

  try {
    // 追加内容(mode: FileMode.append)
    await file.writeAsString('\n这是追加的内容', mode: FileMode.append);
    print('内容追加成功');
  } catch (e) {
    print('追加失败:$e');
  }
}
(3)写入字节数据

根目录下放入一张original_image.png图片,对于二进制文件(如图片、音频),使用 writeAsBytes()

import 'dart:io';

void main() async {
  final imageFile = File('image_copy.png');
  final sourceFile = File('original_image.png');

  try {
    // 读取字节数据
    List<int> bytes = await sourceFile.readAsBytes();
    // 写入字节数据
    await imageFile.writeAsBytes(bytes);
    print('二进制文件复制成功');
  } catch (e) {
    print('文件复制失败:$e');
  }
}

4. 目录操作

除了文件,dart:io 也支持目录的创建、删除和遍历:

import 'dart:io';

void main() async {
  // 定义目录路径
  final dir = Directory('my_files');

  // 检查目录是否存在
  if (!await dir.exists()) {
    // 创建目录(recursive: true 表示创建多级目录)
    await dir.create(recursive: true);
    print('目录创建成功');
  }

  // 遍历目录中的文件和子目录
  await for (FileSystemEntity entity in dir.list()) {
    if (entity is File) {
      print('文件:${entity.path}');
    } else if (entity is Directory) {
      print('目录:${entity.path}');
    }
  }

  // 删除目录(recursive: true 表示删除目录及其中所有内容)
  // await dir.delete(recursive: true);
  // print('目录删除成功');
}


二、命令行参数解析:使用 args 包

开发命令行工具时,通常需要解析用户输入的参数(如 --help-o output.txt 等)。args 包是 Dart 生态中处理命令行参数的首选工具。

1. 添加 args 依赖

在 pubspec.yaml 中添加依赖:

dependencies:
  args: ^2.7.0

执行 dart pub get 安装依赖。

2. 基本参数解析

import 'package:args/args.dart';

void main(List<String> arguments) {
  // 创建参数解析器
  final parser = ArgParser()
    ..addFlag('help', abbr: 'h', help: '显示帮助信息', negatable: false)
    ..addOption('output', abbr: 'o', help: '指定输出文件路径')
    ..addOption(
      'mode',
      help: '运行模式',
      allowed: ['debug', 'release'],
      defaultsTo: 'debug',
    );

  try {
    // 解析参数
    final results = parser.parse(arguments);

    // 处理 --help 参数
    if (results['help'] as bool) {
      print('使用方法:dart script.dart [选项]');
      print(parser.usage);
      return;
    }

    // 获取其他参数值
    print('输出文件:${results['output'] ?? '未指定'}');
    print('运行模式:${results['mode']}');
    print('位置参数:${results.rest}'); // 未解析的位置参数
  } on FormatException catch (e) {
    print('参数错误:$e');
    print('使用 --help 查看帮助');
  }
}

参数类型说明:

  • addFlag:布尔值参数(如 --help),abbr 表示短选项(如 -h
  • addOption:带值的选项(如 --output file.txt
  • results.rest:获取未解析的位置参数

运行示例:

dart script.dart -o result.txt --mode release input1.txt input2.txt

输出:

输出文件:result.txt
运行模式:release
位置参数:[input1.txt, input2.txt]


三、实战:批量重命名文件脚本

结合文件操作和命令行参数解析,我们来开发一个实用的批量重命名工具。该工具支持:

  • 指定目标目录
  • 添加前缀 / 后缀
  • 按序号重命名
  • 预览重命名效果(不实际修改)

完整代码实现

import 'dart:io';
import 'package:args/args.dart';
import 'package:path/path.dart' as path;

void main(List<String> arguments) async {
  // 创建参数解析器
  final parser = ArgParser()
    ..addFlag('help', abbr: 'h', help: '显示帮助信息', negatable: false)
    ..addFlag('preview', abbr: 'p', help: '预览重命名效果,不实际修改', negatable: false)
    ..addOption('dir', abbr: 'd', help: '目标目录路径', defaultsTo: '.')
    ..addOption('prefix', abbr: 'x', help: '文件名前缀')
    ..addOption('suffix', abbr: 's', help: '文件名后缀')
    ..addFlag('number', abbr: 'n', help: '添加序号', negatable: false)
    ..addOption('start', abbr: 't', help: '序号起始值', defaultsTo: '1');

  try {
    final results = parser.parse(arguments);

    // 显示帮助信息
    if (results['help'] as bool) {
      print('批量重命名工具');
      print('用法:dart rename.dart [选项]');
      print(parser.usage);
      return;
    }

    // 解析参数
    final previewMode = results['preview'] as bool;
    final targetDir = Directory(results['dir'] as String);
    final prefix = results['prefix'] as String? ?? '';
    final suffix = results['suffix'] as String? ?? '';
    final addNumber = results['number'] as bool;
    final startNumber = int.tryParse(results['start'] as String) ?? 1;

    // 检查目录是否存在
    if (!await targetDir.exists()) {
      print('错误:目录不存在 - ${targetDir.path}');
      return;
    }

    // 获取目录中的文件(排除子目录)
    final files = await targetDir
        .list()
        .where((entity) => entity is File)
        .toList();
    files.sort((a, b) => a.path.compareTo(b.path)); // 按路径排序

    if (files.isEmpty) {
      print('目录中没有文件:${targetDir.path}');
      return;
    }

    // 批量重命名
    print('${previewMode ? '预览' : '执行'}重命名(共 ${files.length} 个文件):');
    int currentNumber = startNumber;

    for (final file in files.cast<File>()) {
      // 获取文件名和扩展名
      final fileName = path.basename(file.path);
      final extension = fileName.contains('.')
          ? '.${fileName.split('.').last}'
          : '';
      final baseName = extension.isNotEmpty
          ? fileName.substring(0, fileName.length - extension.length)
          : fileName;

      // 构建新文件名
      String newName = '$prefix$baseName$suffix';
      if (addNumber) {
        newName = '${newName}_$currentNumber';
        currentNumber++;
      }
      newName += extension;

      // 构建新路径
      final newPath = '${targetDir.path}${Platform.pathSeparator}$newName';

      // 显示或执行重命名
      print('${file.path}$newPath');
      if (!previewMode && file.path != newPath) {
        await file.rename(newPath);
      }
    }

    print('${previewMode ? '预览' : '重命名'}完成');
  } on FormatException catch (e) {
    print('参数错误:$e');
    print('使用 --help 查看帮助');
  } catch (e) {
    print('操作失败:$e');
  }
}

代码解析

  1. 参数定义:通过 args 包定义了多种参数,满足不同重命名需求。
  2. 目录检查:验证目标目录是否存在,避免错误。
  3. 文件处理
    • 过滤出目录中的文件(排除子目录)
    • 按路径排序,确保重命名顺序一致
  4. 文件名构建
    • 分离文件名和扩展名
    • 根据前缀、后缀、序号等参数生成新文件名
  5. 执行重命名:支持预览模式(仅显示效果)和实际执行模式。

使用示例

  1. 预览当前目录文件添加前缀 "photo_" 并加序号的效果:
dart rename.dart -p -x photo_ -n
  1. 对 images 目录的文件添加后缀 "_edited":
dart rename.dart -d images -s _edited


四、其他实用文件操作技巧

1. 获取文件信息

import 'dart:io';

void printFileInfo(File file) async {
  final stat = await file.stat();
  print('路径:${file.path}');
  print('大小:${stat.size} 字节');
  print('修改时间:${stat.modified}');
  print('是否为文件:${stat.type == FileSystemEntityType.file}');
}

2. 复制文件

import 'dart:io';

Future<void> copyFile(String sourcePath, String targetPath) async {
  final source = File(sourcePath);
  final target = File(targetPath);

  if (await source.exists()) {
    await source.copy(targetPath);
    print('复制成功:$targetPath');
  } else {
    print('源文件不存在:$sourcePath');
  }
}

3. 递归遍历目录

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

Future<void> listAllFiles(Directory dir, {int indent = 0}) async {
  final spaces = '  ' * indent;

  await for (final entity in dir.list()) {
    if (entity is Directory) {
      print('${spaces}目录:${entity.path}');
      // 递归遍历子目录
      await listAllFiles(entity, indent: indent + 1);
    } else if (entity is File) {
      print('${spaces}文件:${path.basename(entity.path)}');
    }
  }
}