// Dart port of splitCommand_DEPRECATED from old_repo/utils/bash/commands.ts. // // Splits a bash compound command into individual subcommands by tokenizing // the input the same way a shell-quote parser would — respecting single/double // quotes, backslash escapes, and the standard control operators. Redirections // (>, >>, >&, 2>&1, etc.) are stripped so they don't appear as separate // "commands" in the permission-prompt list. const _controlOperators = { '&&', '||', ';', ';;', '|', }; // Control + redirection operators that are filtered out when building the final // subcommand list. const _allSupportedOperators = { '&&', '||', ';', ';;', '|', '>&', '>', '>>', }; const _allowedFileDescriptors = {'0', '1', '2'}; /// Checks whether a redirect target is a plain static path (no shell expansion). bool _isStaticRedirectTarget(String target) { if (target.contains(RegExp(r"[\s'" + '"' + "]"))) return false; if (target.isEmpty) return false; if (target.startsWith('#')) return false; return !target.startsWith('!') && !target.startsWith('=') && !target.contains('\$') && !target.contains('`') && !target.contains('*') && !target.contains('?') && !target.contains('[') && !target.contains('{') && !target.contains('~') && !target.contains('(') && !target.contains('<') && !target.startsWith('&'); } /// Tokenizes a bash command string into a flat list of token strings and /// operator strings. Handles single-quoted, double-quoted, and /// backslash-escaped input. Newlines outside quotes are treated as ; /// separators (same as bash IFS behaviour in non-interactive mode). /// /// Returns each token or operator as a separate list element. List _tokenize(String command) { final tokens = []; final buf = StringBuffer(); int i = 0; final len = command.length; void flush() { final s = buf.toString(); if (s.isNotEmpty) { tokens.add(s); buf.clear(); } } while (i < len) { final c = command[i]; if (c == '\\' && i + 1 < len) { // backslash-newline: line continuation — skip both chars if (command[i + 1] == '\n') { i += 2; continue; } // otherwise backslash-escape: absorb both chars into current token buf.write(c); buf.write(command[i + 1]); i += 2; continue; } if (c == "'") { // single-quoted: everything literal until next ' buf.write(c); i++; while (i < len && command[i] != "'") { buf.write(command[i]); i++; } if (i < len) { buf.write(command[i]); // closing ' i++; } continue; } if (c == '"') { // double-quoted: allow \x escapes but nothing else is special buf.write(c); i++; while (i < len && command[i] != '"') { if (command[i] == '\\' && i + 1 < len) { buf.write(command[i]); buf.write(command[i + 1]); i += 2; } else { buf.write(command[i]); i++; } } if (i < len) { buf.write(command[i]); // closing " i++; } continue; } // Newline outside quotes acts as a command separator if (c == '\n') { flush(); tokens.add(';'); // treat as ; i++; continue; } // Whitespace separates tokens if (c == ' ' || c == '\t') { flush(); i++; continue; } // check for two-char operators first: &&, ||, >>, >&, ;; if (i + 1 < len) { final two = command.substring(i, i + 2); if (two == '&&' || two == '||' || two == '>>' || two == '>&' || two == ';;') { flush(); tokens.add(two); i += 2; continue; } } // single-char operators: ;, |, > if (c == ';' || c == '|' || c == '>') { flush(); tokens.add(c); i++; continue; } // Comment — skip to end of line if (c == '#' && buf.isEmpty) { while (i < len && command[i] != '\n') { i++; } continue; } buf.write(c); i++; } flush(); return tokens; } /// Splits a compound bash command into individual subcommands by tokenizing /// and then stripping redirections and filtering out control operators. /// /// This is the Dart equivalent of splitCommand_DEPRECATED in commands.ts. List splitCommand(String command) { final tokens = _tokenize(command); // Strip output redirections (>, >>, >&, 2>&1 etc.) // Same logic as the JS implementation — mark tokens for removal. final parts = List.from(tokens); for (int i = 0; i < parts.length; i++) { final part = parts[i]; if (part == null) continue; if (part == '>&' || part == '>' || part == '>>') { final prevPart = i > 0 ? parts[i - 1]?.trimRight() : null; final nextPart = i + 1 < parts.length ? parts[i + 1]?.trim() : null; final afterNext = i + 2 < parts.length ? parts[i + 2]?.trim() : null; if (nextPart == null) continue; bool shouldStrip = false; bool stripThird = false; if (part == '>&' && _allowedFileDescriptors.contains(nextPart)) { // 2>&1 style shouldStrip = true; } else if (part == '>' && nextPart == '&' && afterNext != null && _allowedFileDescriptors.contains(afterNext)) { shouldStrip = true; stripThird = true; } else if (part == '>' && nextPart.startsWith('&') && nextPart.length > 1 && _allowedFileDescriptors.contains(nextPart.substring(1))) { shouldStrip = true; } else if ((part == '>' || part == '>>') && _isStaticRedirectTarget(nextPart)) { shouldStrip = true; } if (shouldStrip) { // strip trailing file descriptor from previous token (e.g., '2' from 'echo foo 2') if (prevPart != null && prevPart.length >= 3 && _allowedFileDescriptors.contains(prevPart[prevPart.length - 1]) && prevPart[prevPart.length - 2] == ' ') { parts[i - 1] = prevPart.substring(0, prevPart.length - 2); } parts[i] = null; parts[i + 1] = null; if (stripThird && i + 2 < parts.length) { parts[i + 2] = null; } } } } // Filter nulls and empty strings, then filter control operators return parts .where((p) => p != null && p.isNotEmpty) .cast() .where((p) => !_controlOperators.contains(p)) .where((p) => !_allSupportedOperators.contains(p)) .toList(); } /// Returns true when the command contains a cd subcommand (used by the /// cd + git security check in pathValidation). bool commandHasAnyCd(String command) { final subs = splitCommand(command); return subs.any((s) { final trimmed = s.trim(); return trimmed == 'cd' || trimmed.startsWith('cd ') || trimmed.startsWith('cd\t'); }); } /// Returns true when the given subcommand (already split) is a plain cd invocation. bool isNormalizedCdCommand(String command) { final trimmed = command.trim(); return trimmed == 'cd' || trimmed.startsWith('cd ') || trimmed.startsWith('cd\t'); } /// Returns true when the given subcommand is a git invocation /// (with optional leading safe env var assignments stripped off). bool isNormalizedGitCommand(String command) { // strip safe env vars at the front — simple heuristic var trimmed = command.trim(); while (true) { final m = RegExp(r'^[A-Za-z_]\w*=[^ \t]* ').firstMatch(trimmed); if (m == null) break; trimmed = trimmed.substring(m.end); } return trimmed == 'git' || trimmed.startsWith('git ') || trimmed.startsWith('git\t'); }