329 lines
20 KiB
Dart
329 lines
20 KiB
Dart
import 'dart:io' show Platform;
|
||
|
||
import '../../l10n/tr.dart';
|
||
|
||
// 약속 (나누기 sync_tools): rsync 옵션 칸의 글 - 붙여 넣은 이상한 글자 고치기 · 인수로 나누기 · rsync 가 다르게 읽을 것 찾기 · 목록 파일 값.
|
||
// 입력: 옵션 글 (사용자가 적거나 붙여 넣은 것) · 낱말 목록 · Windows 인지 (null 이면 이 기기).
|
||
// 출력: 낱말 목록 · 문제 목록 ([OptionProblem]) · 고칠 방법 글. 오류: 던지지 않는다 (알아볼 수 없는 것은 문제 목록으로 돌려준다).
|
||
|
||
/// 관리자 지시 (10/20): 화면에서 줄바꿈 없이 묶은 옵션 글 (sync_preview_view 의 unbreakableOption) 의 줄바꿈 없는 빈칸 (U+00A0 등) ·
|
||
/// 보이지 않는 글자 (U+2060 등) 가 복사 · 붙여 넣기로 옵션에 들어오면 rsync 가 다른 인수로 읽는다 ("-", 보이지 않는 글자, "-filter") → 보통 빈칸으로 / 지움.
|
||
/// 다른 곳 (웹 페이지 · 문서) 에서 붙여 넣은 것도 같다
|
||
/// 28 확인 (10/20): 소프트 하이픈 (U+00AD) 은 지우고 (rsync 가 "unknown option" 으로 실패), 전각 빈칸 (U+3000) 도 보통 빈칸으로
|
||
String cleanOptionText(String s) => s
|
||
.replaceAll(RegExp('[\u00A0\u2007\u202F\u3000]'), ' ')
|
||
.replaceAll(RegExp('[\u00AD\u200B\u200C\u200D\u2060\uFEFF]'), '');
|
||
|
||
/// 낱말 맨 앞에서 - 로 읽는 대시 (‐ ‑ ‒ – — ― − ﹘ ﹣ -) - 워드 · 웹이 -- 를 바꿔 놓은 것
|
||
const _dashLike = '\u2010\u2011\u2012\u2013\u2014\u2015\u2212\uFE58\uFE63\uFF0D';
|
||
|
||
/// 굽은 따옴표 → 곧은 따옴표 (같은 무리끼리 열고 닫는다)
|
||
const _curlyQuote = {'\u2018': "'", '\u2019': "'", '\u201A': "'", '\u201C': '"', '\u201D': '"', '\u201E': '"'};
|
||
|
||
/// 옵션 글자를 인수로 나눈다 (큰따옴표 · 작은따옴표로 묶은 것은 하나로). 먼저 [cleanOptionText].
|
||
/// 관리자 지시 (10/20, 28 · 15 확인): 웹 · 문서에서 붙여 넣은 글을 곧은 글자로 쓴 것과 같게 읽는다 - 예전에는
|
||
/// --exclude=‘*.log’ 가 아무것도 거르지 못해 받는 쪽 keep.log 가 지워졌고, –delete 는 원본 경로로 넘어가 --delete 가 몰래 빠졌다.
|
||
/// - 굽은 따옴표 (‘ ’ “ ”) 는 낱말 맨 앞 · = 뒤 · 짧은 옵션 (-f) 뒤에서 열고, 같은 무리의 굽은 따옴표에서 닫는다
|
||
/// (곧은 따옴표처럼 뒤에 글이 이어져도 - “a b”.txt → a b.txt). 작은따옴표 무리는 글자 사이 (it’s) 이면 닫지 않는다.
|
||
/// 이름 안의 굽은 따옴표 (it’s.txt) 는 그대로 둔다.
|
||
/// - 닫히지 않은 따옴표 (15 확인 F1): 예전에는 뒤의 옵션을 모두 삼켜 지키려던 --exclude 가 사라졌다 → 그 따옴표를 글자로 보고
|
||
/// 나머지는 그대로 나누며, [unreadableOptions] 가 알려 실행을 막는다.
|
||
/// - 낱말 맨 앞의 대시 (– — ‑ - 등, 따옴표 밖): 둘이면 --, 하나면 rsync 의 긴 옵션 이름 (–delete) 일 때만 -- 그 밖 (–avz) 은 -.
|
||
/// 옵션의 값 자리 (--exclude –x.txt 의 –x.txt) · 낱말 가운데의 대시는 그대로 (파일 이름). --filter · -f 값의 규칙 머리 (“– *.tmp”) 는 - 로.
|
||
List<String> splitOptions(String s) => _parseOptions(s).words;
|
||
|
||
/// rsync 3.x 의 긴 옵션 이름 (앱에 든 rsync 3.5.1 의 --help) - 대시 하나로 붙여 넣은 –delete 를 --delete 로 읽을지 정한다
|
||
const _rsyncLong = {
|
||
'acls', 'address', 'append', 'append-verify', 'archive', 'atimes', 'backup', 'backup-dir', 'block-size', 'blocking-io',
|
||
'bwlimit', 'cc', 'checksum', 'checksum-choice', 'checksum-seed', 'chmod', 'chown', 'compare-dest', 'compress',
|
||
'compress-choice', 'compress-level', 'compress-threads', 'confine-root', 'contimeout', 'copy-as', 'copy-dest',
|
||
'copy-devices', 'copy-dirlinks', 'copy-links', 'copy-unsafe-links', 'crtimes', 'cvs-exclude', 'daemon', 'debug', 'del',
|
||
'delay-updates', 'delete', 'delete-after', 'delete-before', 'delete-delay', 'delete-during', 'delete-excluded',
|
||
'delete-missing-args', 'devices', 'dirs', 'dry-run', 'early-input', 'exclude', 'exclude-from', 'executability', 'existing',
|
||
'fake-super', 'files-from', 'filter', 'force', 'from0', 'fsync', 'fuzzy', 'group', 'groupmap', 'hard-links', 'help',
|
||
'human-readable', 'i-r', 'iconv', 'ignore-errors', 'ignore-existing', 'ignore-missing-args', 'ignore-non-existing',
|
||
'ignore-times', 'inc-recursive', 'include', 'include-from', 'info', 'inplace', 'insecure-links', 'ipv4', 'ipv6',
|
||
'itemize-changes', 'keep-dirlinks', 'link-dest', 'links', 'list-only', 'log-file', 'log-file-format', 'max-alloc',
|
||
'max-delete', 'max-size', 'min-size', 'mkpath', 'modify-window', 'munge-links', 'no-i-r', 'no-implied-dirs',
|
||
'no-inc-recursive', 'no-motd', 'no-whole-file', 'numeric-ids', 'old-args', 'old-d', 'old-dirs', 'omit-dir-times',
|
||
'omit-link-times', 'one-file-system', 'only-write-batch', 'open-noatime', 'out-format', 'outbuf', 'owner', 'partial',
|
||
'partial-dir', 'password-file', 'perms', 'port', 'preallocate', 'progress', 'protocol', 'prune-empty-dirs', 'quiet',
|
||
'read-batch', 'recursive', 'relative', 'remote-option', 'remove-source-files', 'rsh', 'rsync-path', 'safe-links',
|
||
'secluded-args', 'size-only', 'skip-compress', 'sockopts', 'sparse', 'specials', 'stats', 'stderr', 'stop-after', 'stop-at',
|
||
'suffix', 'super', 'temp-dir', 'timeout', 'times', 'trust-sender', 'update', 'usermap', 'verbose', 'version',
|
||
'whole-file', 'write-batch', 'write-devices', 'xattrs', 'zc', 'zl', 'zt',
|
||
};
|
||
|
||
/// 나눈 낱말: 맨 앞이 따옴표 안이었는지 (그러면 맨 앞 대시를 옵션으로 고치지 않는다)
|
||
typedef _Word = ({String text, bool quoted});
|
||
|
||
/// [splitOptions] · [unreadableOptions] 의 몸통: 낱말들과 닫히지 않은 따옴표 (뒤 글 일부)
|
||
({List<String> words, List<String> unclosed}) _parseOptions(String s) {
|
||
final chars = cleanOptionText(s).split('');
|
||
final literal = <int>{}; // 닫히지 않아 글자로 보는 따옴표 자리
|
||
final unclosed = <String>[];
|
||
while (true) {
|
||
final (raw, open) = _splitRaw(chars, literal);
|
||
if (open == null) return (words: _normalizeWords(raw), unclosed: unclosed);
|
||
literal.add(open);
|
||
final rest = chars.sublist(open).join().trim();
|
||
unclosed.add(rest.length > 30 ? '${rest.substring(0, 30)}…' : rest);
|
||
}
|
||
}
|
||
|
||
/// 따옴표 · 빈칸으로 나눈다. 끝까지 닫히지 않은 따옴표가 있으면 그 자리 (두 번째 값)
|
||
(List<_Word>, int?) _splitRaw(List<String> chars, Set<int> literal) {
|
||
final out = <_Word>[];
|
||
final cur = StringBuffer();
|
||
String? quote; // 닫는 곧은 따옴표 (" 또는 ')
|
||
var curly = false; // 굽은 따옴표로 열었는지
|
||
var openAt = -1;
|
||
var has = false;
|
||
var quoted = false;
|
||
bool letter(int i) => i >= 0 && i < chars.length && RegExp(r'[\p{L}\p{N}]', unicode: true).hasMatch(chars[i]);
|
||
for (var i = 0; i < chars.length; i++) {
|
||
final ch = chars[i];
|
||
if (quote != null) {
|
||
// 작은따옴표 무리의 ’ 가 글자 사이면 (it’s) 닫지 않는다
|
||
final closes = ch == quote && !curly ||
|
||
curly && _curlyQuote[ch] == quote && !(quote == "'" && letter(i - 1) && letter(i + 1));
|
||
if (closes) {
|
||
quote = null;
|
||
curly = false;
|
||
} else {
|
||
cur.write(ch);
|
||
}
|
||
} else if (literal.contains(i)) {
|
||
cur.write(ch);
|
||
} else if (ch == '"' || ch == "'") {
|
||
if (cur.isEmpty && !has) quoted = true;
|
||
quote = ch;
|
||
openAt = i;
|
||
has = true;
|
||
} else if (_curlyQuote.containsKey(ch) && (cur.isEmpty || '$cur'.endsWith('=') || RegExp(r'^-[A-Za-z0-9]+$').hasMatch('$cur'))) {
|
||
if (cur.isEmpty && !has) quoted = true;
|
||
quote = _curlyQuote[ch];
|
||
curly = true;
|
||
openAt = i;
|
||
has = true;
|
||
} else if (ch.trim().isEmpty) {
|
||
if (cur.isNotEmpty || has) out.add((text: cur.toString(), quoted: quoted));
|
||
cur.clear();
|
||
has = false;
|
||
quoted = false;
|
||
} else {
|
||
cur.write(ch);
|
||
}
|
||
}
|
||
if (quote != null) return (out, openAt);
|
||
if (cur.isNotEmpty || has) out.add((text: cur.toString(), quoted: quoted));
|
||
return (out, null);
|
||
}
|
||
|
||
bool _dashAt(String t, int i) => i < t.length && (t[i] == '-' || _dashLike.contains(t[i]));
|
||
|
||
/// 거르기 규칙 머리 ("– *.tmp" · "+ *.txt") 를 rsync 가 읽는 - · + 로
|
||
String _fixRuleHead(String v) {
|
||
if (v.isEmpty) return v;
|
||
if (_dashLike.contains(v[0])) return '-${v.substring(1)}';
|
||
if (v[0] == '\uFF0B') return '+${v.substring(1)}';
|
||
return v;
|
||
}
|
||
|
||
/// 낱말 맨 앞 대시 고치기 · 거르기 값의 규칙 머리 고치기 (값 자리는 그대로)
|
||
List<String> _normalizeWords(List<_Word> raw) {
|
||
final out = <String>[];
|
||
var value = ''; // 다음 낱말이 값: 'filter' (규칙 머리를 고침) · 'other' (그대로)
|
||
for (final w in raw) {
|
||
var t = w.text;
|
||
if (value.isNotEmpty) {
|
||
out.add(value == 'filter' ? _fixRuleHead(t) : t);
|
||
value = '';
|
||
continue;
|
||
}
|
||
// 28 확인 (10/20): 곧은 하이픈과 섞인 것 (-–delete) 도 맨 앞 둘 중 하나라도 대시면 같은 규칙으로
|
||
if (!w.quoted && t.isNotEmpty && (_dashLike.contains(t[0]) || t.length > 1 && t[0] == '-' && _dashLike.contains(t[1]))) {
|
||
final n = _dashAt(t, 1) ? 2 : 1;
|
||
final rest = t.substring(n);
|
||
final name = rest.split('=').first;
|
||
final long = n == 2 || _rsyncLong.contains(name) || (name.startsWith('no-') && _rsyncLong.contains(name.substring(3)));
|
||
t = '${long ? '--' : '-'}$rest';
|
||
}
|
||
if (t.startsWith('--filter=')) {
|
||
t = '--filter=${_fixRuleHead(t.substring(9))}';
|
||
} else if (t.length > 1 && t.startsWith('-') && !t.startsWith('--')) {
|
||
final k = t.indexOf(RegExp('[BefMT@]'), 1);
|
||
if (k > 0 && t[k] == 'f' && k < t.length - 1) t = '${t.substring(0, k + 1)}${_fixRuleHead(t.substring(k + 1))}';
|
||
}
|
||
out.add(t);
|
||
if (t.startsWith('--')) {
|
||
if (!t.contains('=') && _rsyncLongWithValue.contains(t)) value = t == '--filter' ? 'filter' : 'other';
|
||
} else if (t.length > 1 && t.startsWith('-')) {
|
||
final k = t.indexOf(RegExp('[BefMT@]'), 1);
|
||
if (k == t.length - 1) value = t[k] == 'f' ? 'filter' : 'other';
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/// 값을 다음 낱말로 받는 rsync 긴 옵션 (--exclude *.log) - [unreadableOptions] 가 그 값을 옵션으로 보지 않게
|
||
const _rsyncLongWithValue = {
|
||
'--exclude', '--include', '--filter', '--exclude-from', '--include-from', '--files-from', '--backup-dir', '--suffix',
|
||
'--log-file', '--log-file-format', '--out-format', '--chmod', '--chown', '--usermap', '--groupmap', '--bwlimit',
|
||
'--timeout', '--contimeout', '--rsh', '--rsync-path', '--compare-dest', '--copy-dest', '--link-dest', '--partial-dir',
|
||
'--temp-dir', '--max-size', '--min-size', '--max-delete', '--modify-window', '--password-file', '--iconv',
|
||
'--block-size', '--checksum-choice', '--compress-choice', '--compress-level', '--skip-compress', '--info', '--debug',
|
||
'--sockopts', '--port', '--address', '--outbuf', '--remote-option', '--max-alloc', '--stop-after', '--stop-at',
|
||
'--write-batch', '--only-write-batch', '--read-batch', '--protocol', '--checksum-seed', '--copy-as', '--early-input',
|
||
// 28 확인 (10/20): rsync --help 의 짧은 별칭 (aka --cc · --zc · --zl · --zt) 과 값 받는 옵션 - 빠져 있어 값이 "경로로 읽힙니다" 로 막혔다
|
||
'--cc', '--zc', '--zl', '--zt', '--compress-threads', '--confine-root',
|
||
};
|
||
|
||
/// 관리자 지시 (10/20, 28 · 15 확인): [splitOptions] 로 고쳐 읽은 뒤에도 rsync 가 다르게 읽을 것 - 있으면 확인 창 · 실시간 동기화에
|
||
/// 빨갛게 알리고 실행을 막는다: 닫히지 않은 따옴표, 옵션 자리의 - 로 시작하지 않는 낱말 (rsync 는 원본 경로로 읽어 옵션이 몰래 빠짐),
|
||
/// 하이픈 셋 이상, 끝에 값 없이 남은 값 받는 옵션. 옵션의 값 자리 (--exclude 한글.txt 의 한글.txt, -f 뒤) 는 세지 않는다
|
||
List<String> unreadableOptions(String options, {bool? windows}) => [for (final x in optionProblems(options, windows: windows)) x.text];
|
||
|
||
/// 옵션 칸의 문제 종류 - 안내의 끝 문구 (고칠 방법) 를 고른다 (관리자 지시 10/20, 28 지적)
|
||
enum OptionProblemKind { unclosed, path, noValue, odd, doubleQuote, relativePath, stdin }
|
||
|
||
/// [unreadableOptions] 의 한 줄과 그 종류
|
||
class OptionProblem {
|
||
final OptionProblemKind kind;
|
||
final String text;
|
||
|
||
/// [text] 안의 옵션 낱말 (화면이 줄바꿈 없이 묶는다)
|
||
final String token;
|
||
const OptionProblem(this.kind, this.text, [this.token = '']);
|
||
|
||
@override
|
||
String toString() => text;
|
||
}
|
||
|
||
/// 문제 종류에 맞는 고칠 방법 (여러 종류면 · 로 잇는다)
|
||
String optionProblemHint(Iterable<OptionProblem> problems, {bool? windows}) {
|
||
windows ??= Platform.isWindows;
|
||
final kinds = {for (final x in problems) x.kind};
|
||
return [
|
||
if (kinds.contains(OptionProblemKind.unclosed)) tr('따옴표를 닫아 주세요'),
|
||
if (kinds.contains(OptionProblemKind.path)) tr('옵션은 - 나 -- 로 시작합니다'),
|
||
if (kinds.contains(OptionProblemKind.noValue)) tr('값을 적어 주세요 (예: --exclude=*.tmp)'),
|
||
if (kinds.contains(OptionProblemKind.odd)) tr('곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요'),
|
||
if (kinds.contains(OptionProblemKind.doubleQuote)) tr('큰따옴표 (") 를 빼 주세요'),
|
||
// 관리자 지시 (10/20): Windows 는 드라이브로 시작하는 전체 경로만 ("/" 는 rsync 가 자기 설치 폴더 기준으로 읽음), Android 는 /
|
||
if (kinds.contains(OptionProblemKind.relativePath)) windows ? tr('C:\\ 처럼 드라이브로 시작하는 전체 경로로') : tr('/ 로 시작하는 전체 경로로'),
|
||
if (kinds.contains(OptionProblemKind.stdin)) tr('목록 파일의 전체 경로를 적어 주세요'),
|
||
].join(' · ');
|
||
}
|
||
|
||
/// "옵션을 알아볼 수 없습니다: … - 고칠 방법" (작업 기록 · 실시간 동기화 카드 · 설정 칸)
|
||
String optionProblemMessage(List<OptionProblem> problems) =>
|
||
trf('옵션을 알아볼 수 없습니다: {0} - {1}', [problems.join(' · '), optionProblemHint(problems)]);
|
||
|
||
/// [unreadableOptions] 의 몸통 (종류와 함께)
|
||
List<OptionProblem> optionProblems(String options, {bool? windows}) {
|
||
windows ??= Platform.isWindows;
|
||
final parsed = _parseOptions(options);
|
||
// 15 확인 F1: 닫히지 않은 따옴표 - 뒤의 옵션이 다르게 읽힐 수 있다
|
||
final bad = [for (final u in parsed.unclosed) OptionProblem(OptionProblemKind.unclosed, trf('따옴표가 닫히지 않았습니다: {0}', [u]), u)];
|
||
var value = false;
|
||
var last = '';
|
||
for (final t in parsed.words) {
|
||
// P0 (10/20): Windows 의 rsync (MSYS2) 에는 큰따옴표가 든 인수를 그대로 넘길 수 없다 (prepareRsyncLaunch) - 값 자리도.
|
||
// 닫히지 않은 따옴표로 이미 막혔으면 (그 따옴표가 낱말에 남음) 한 번만 알린다
|
||
if (windows && parsed.unclosed.isEmpty && t.contains('"')) {
|
||
bad.add(OptionProblem(OptionProblemKind.doubleQuote, trf('큰따옴표 (") 가 든 옵션은 Windows 의 rsync 에 넘길 수 없습니다: {0}', [t]), t));
|
||
value = false;
|
||
continue;
|
||
}
|
||
if (value) {
|
||
value = false;
|
||
continue;
|
||
}
|
||
if (t.startsWith('---')) {
|
||
// 관리자 판단 (10/20, 28 확인): 하이픈이 셋 이상 (---delete) 은 rsync 가 알아보지 못한다 - 끝 문구는 "옵션은 - 나 -- 로 시작합니다"
|
||
bad.add(OptionProblem(OptionProblemKind.path, t, t));
|
||
} else if (t.startsWith('--')) {
|
||
value = !t.contains('=') && _rsyncLongWithValue.contains(t);
|
||
last = t;
|
||
} else if (t.length > 1 && t.startsWith('-')) {
|
||
// 짧은 옵션 묶음: 값을 받는 글자 (B · e · f · M · T · @) 가 끝이면 다음 낱말이 값 (rsync_filter 와 같은 규칙)
|
||
final k = t.indexOf(RegExp('[BefMT@]'), 1);
|
||
value = k == t.length - 1;
|
||
last = t;
|
||
} else {
|
||
// 관리자 판단 (10/20, 28 확인): 옵션 칸에는 경로가 들어갈 자리가 없다 (원본 · 대상은 앱이 넣음) - 값 자리가 아닌데
|
||
// - 로 시작하지 않는 낱말 (-a delete 의 delete, 혼자 쓴 -) 은 rsync 가 원본 경로로 읽어 옵션이 몰래 빠진다
|
||
// ASCII 밖 글자로 시작하면 (« … 등) 붙여 넣은 이상한 글자 - 안내는 곧은 글자로 다시 적기
|
||
bad.add(OptionProblem(t.codeUnitAt(0) > 0x7F ? OptionProblemKind.odd : OptionProblemKind.path, trf('경로로 읽힙니다: {0}', [t]), t));
|
||
}
|
||
}
|
||
// 값이 필요한 옵션이 끝에 값 없이 (--exclude) - rsync 가 원본 경로를 그 값으로 먹는다
|
||
if (value) bad.add(OptionProblem(OptionProblemKind.noValue, trf('{0} 의 값이 없습니다', [last]), last));
|
||
// 관리자 판단 (10/20): 목록 파일의 상대 경로 - rsync 는 자기 작업 폴더 (Windows 는 응답 파일 폴더) 기준으로, 미리 보기는 앱의
|
||
// 작업 폴더 기준으로 읽어 서로 다른 파일을 볼 수 있다. 전체 경로만 받는다
|
||
for (final f in listFileValues(parsed.words)) {
|
||
// 관리자 지시 (10/20, 28 지적): "-" (표준 입력) 는 따로 - 앱은 rsync 에 표준 입력을 주지 않는다
|
||
if (f == '-') {
|
||
bad.add(OptionProblem(OptionProblemKind.stdin, tr('표준 입력 (-) 은 쓸 수 없습니다'), f));
|
||
} else if (!_absolutePath(f, windows: windows)) {
|
||
bad.add(OptionProblem(OptionProblemKind.relativePath, trf('목록 파일은 전체 경로로 적어 주세요: {0}', [f]), f));
|
||
}
|
||
}
|
||
// 관리자 지시 (10/20, 28 확인): 기록 · 배치 파일도 - 상대 경로면 응답 파일 임시 폴더에 생겼다가 함께 지워져 찾을 수 없다.
|
||
// --backup-dir · --partial-dir · --temp-dir · --compare-dest 류는 받는 쪽 기준이라 그대로
|
||
for (final f in _optionValues(parsed.words, const {'--log-file', '--write-batch', '--read-batch', '--only-write-batch'})) {
|
||
if (!_absolutePath(f, windows: windows)) bad.add(OptionProblem(OptionProblemKind.relativePath, trf('기록 · 배치 파일은 전체 경로로 적어 주세요: {0}', [f]), f));
|
||
}
|
||
return bad;
|
||
}
|
||
|
||
/// 긴 옵션 [names] 의 값 (--이름=값 · --이름 값)
|
||
List<String> _optionValues(List<String> words, Set<String> names) => [
|
||
for (var i = 0; i < words.length; i++)
|
||
if (words[i].contains('=') && names.contains(words[i].substring(0, words[i].indexOf('='))))
|
||
words[i].substring(words[i].indexOf('=') + 1)
|
||
else if (names.contains(words[i]) && i + 1 < words.length)
|
||
words[i + 1],
|
||
];
|
||
|
||
/// 파일을 받는 옵션의 값 (--exclude-from · --include-from · --files-from · --filter / -f 의 ". 파일" · "merge 파일").
|
||
/// ": 이름" · dir-merge 는 폴더마다 읽는 파일 이름이라 (원래 상대) 빼고
|
||
List<String> listFileValues(List<String> words) {
|
||
const fileOpts = {'--exclude-from', '--include-from', '--files-from'};
|
||
final out = <String>[];
|
||
String? mergeFile(String rule) => RegExp(r'^(?:\.|merge)(?:,[^\s]*)?\s+(.+)$').firstMatch(rule.trim())?.group(1);
|
||
for (var i = 0; i < words.length; i++) {
|
||
final w = words[i];
|
||
final next = i + 1 < words.length ? words[i + 1] : null;
|
||
final eq = w.indexOf('=');
|
||
if (w.startsWith('--') && eq > 0 && fileOpts.contains(w.substring(0, eq))) {
|
||
out.add(w.substring(eq + 1));
|
||
} else if (fileOpts.contains(w) && next != null) {
|
||
out.add(next);
|
||
i++;
|
||
} else if (w.startsWith('--filter=')) {
|
||
final f = mergeFile(w.substring(9));
|
||
if (f != null) out.add(f);
|
||
} else if (w == '--filter' && next != null) {
|
||
final f = mergeFile(next);
|
||
if (f != null) out.add(f);
|
||
i++;
|
||
} else if (!w.startsWith('--') && w.length > 1 && w.startsWith('-')) {
|
||
// 짧은 옵션 묶음의 f: 붙은 값 (-f". 목록") 또는 다음 낱말 (-avf ". 목록")
|
||
final k = w.indexOf(RegExp('[BefMT@]'), 1);
|
||
if (k < 0 || w[k] != 'f') continue;
|
||
final rule = k == w.length - 1 ? next : w.substring(k + 1);
|
||
if (k == w.length - 1) i++;
|
||
final f = rule == null ? null : mergeFile(rule);
|
||
if (f != null) out.add(f);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/// 전체 경로. Windows: 드라이브 (C:\ · C:/) · UNC (\\서버 · //서버) · /cygdrive/… - 그 밖의 / 는 MSYS 가 자기 설치 폴더 기준으로 읽어 뺀다.
|
||
/// Android: / 로 시작
|
||
bool _absolutePath(String f, {required bool windows}) =>
|
||
windows ? RegExp(r'^(?:[A-Za-z]:[\\/]|\\\\|//|/cygdrive/)').hasMatch(f) : f.startsWith('/');
|