⌥ Shell 脚本格式化

Bash/Shell 脚本格式化:规范化缩进、折叠空行、对齐注释。支持 if/for/while/case/function 块。

等待输入...
已复制
const SAMPLE = '#!/bin/bash\n# Backup script\nset -e\n\nbackup() {\n local src=$1\n local dest=$2\n if [ -d "$src" ]; then\n tar -czf "$dest" "$src"\n echo "Backup complete"\n else\n echo "Source not found"\n fi\n}\n\nfor dir in /home /var; do\n backup "$dir" "/tmp/$(basename $dir).tar.gz"\ndone\n\ncase $1 in\n start)\n systemctl start nginx\n ;;\n stop)\n systemctl stop nginx\n ;;\n *)\n echo "Usage: $0 {start|stop}"\n ;;\nesac'; const IND = () => { const v = document.getElementById('indent').value; return v === 'tab' ? '\t' : ' '.repeat(parseInt(v)); }; function formatShell(){ setErr(''); try{ const src = document.getElementById('input').value; const lines = src.split('\n'); const out = []; let depth = 0; let inHeredoc = false; let heredocEnd = ''; let prevBlank = false; const indStart = /^(if|for|while|until|case)\b/; const indMid = /^(then|do|else|elif)\b/; const dedMid = /^(fi|done|esac)\b/; const funcOpen = /\{\s*$/; const funcClose = /^\s*\}/; for(const line of lines){ const trimmed = line.trim(); if(inHeredoc){ out.push(line); if(trimmed === heredocEnd || trimmed === heredocEnd.replace(/^['"]|['"]$/g,'')) inHeredoc = false; continue; } const heredocMatch = line.match(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/); if(heredocMatch){ heredocEnd = heredocMatch[2]; inHeredoc = true; out.push(line); continue; } if(trimmed === ''){ if(!prevBlank){ out.push(''); prevBlank = true; } continue; } prevBlank = false; let d = depth; if(dedMid.test(trimmed)) d = Math.max(0, d - 1); if(indMid.test(trimmed) && trimmed.startsWith('esac')) d = Math.max(0, d - 1); else if(indMid.test(trimmed) && (trimmed.startsWith('else') || trimmed.startsWith('elif'))) d = Math.max(0, d - 1); else if(trimmed === ';;' || trimmed === ';&' || trimmed === ';;&') d = Math.max(0, d - 1); const isCasePat = /^([A-Za-z0-9_*?\-|]+\)|.+\))/.test(trimmed) && !trimmed.includes('function'); out.push(IND().repeat(d) + trimmed); if(indStart.test(trimmed) || funcOpen.test(trimmed) || trimmed.endsWith('then') || trimmed.endsWith('do') || trimmed === '{') depth++; if(isCasePat && trimmed.endsWith(')')) depth++; if(trimmed === 'fi' || trimmed === 'done' || trimmed === 'esac' || funcClose.test(trimmed) || trimmed === '}') depth = Math.max(0, depth - 1); if(trimmed === ';;' || trimmed === ';&' || trimmed === ';;&') depth = Math.max(0, depth - 1); } setOut(out.join('\n')); setErr('✅ 格式化成功'); }catch(e){ setErr('❌ ' + e.message); } } function minifyShell(){ setErr(''); try{ const src = document.getElementById('input').value; const lines = src.split('\n').map(l => l.trim()).filter((l, i, arr) => { if(l === '' && (i === 0 || arr[i-1] === '')) return false; return true; }); let out = ''; for(let i = 0; i < lines.length; i++){ const cur = lines[i]; const next = lines[i+1] || ''; const needsSep = cur.endsWith(';') || cur.endsWith('{') || cur.endsWith('(') || cur.endsWith('\\') || cur.endsWith('then') || cur.endsWith('do') || next.startsWith('then') || next.startsWith('do') || next.startsWith('fi') || next.startsWith('done') || next.startsWith('else') || next.startsWith('elif') || next.startsWith('esac') || next.startsWith('}') || next.startsWith(')') || cur.startsWith('#!'); out += cur + (needsSep || cur === '' ? '\n' : '; '); } setOut(out.trim()); setErr('✅ 压缩成功'); }catch(e){ setErr('❌ ' + e.message); } } function doBeautify(){ formatShell(); } function doMinify(){ minifyShell(); } function doValidate(){ setErr(''); try{ const src = document.getElementById('input').value; const opens = (src.match(/\b(if|for|while|until|case)\b/g) || []).length; const closes = (src.match(/\b(fi|done|esac)\b/g) || []).length; const braceO = (src.match(/\{/g) || []).length; const braceC = (src.match(/\}/g) || []).length; const parenO = (src.match(/\(/g) || []).length; const parenC = (src.match(/\)/g) || []).length; const stats = '✅ Shell 脚本分析\n' + '块开头: ' + opens + '\n' + '块结尾: ' + closes + '\n' + '{ 数量: ' + braceO + '\n' + '} 数量: ' + braceC + '\n' + '( 数量: ' + parenO + '\n' + ') 数量: ' + parenC + '\n' + '行数: ' + src.split('\n').length; let ok = true; if(opens !== closes){ ok = false; setErr('❌ 块未平衡:开始 ' + opens + ' / 结束 ' + closes); } else if(braceO !== braceC){ ok = false; setErr('❌ 花括号未平衡:{ ' + braceO + ' / } ' + braceC); } else if(parenO !== parenC){ ok = false; setErr('❌ 圆括号未平衡:( ' + parenO + ' / ) ' + parenC); } else { setErr('✅ 校验通过'); } setOut(stats); }catch(e){ setErr('❌ ' + e.message); } } function getIndent(){const v=document.getElementById('indent').value;return v==='tab'?'\t':' '.repeat(parseInt(v));} function setOut(s){document.getElementById('output').textContent=s;} function setErr(s){const e=document.getElementById('err');e.textContent=s||'';e.className='err'+(s?'':' ok');} function clearAll(){document.getElementById('input').value='';setOut('等待输入...');setErr('');} function loadSample(){document.getElementById('input').value=SAMPLE;doBeautify();}