[{"data":1,"prerenderedAt":3783},["ShallowReactive",2],{"all-lessons":3},[4,1091,1482,2098,2363,3075,3497],{"id":5,"title":6,"body":7,"date":1073,"description":1074,"difficulty":1075,"extension":1076,"format":1077,"meta":1078,"navigation":154,"path":1079,"progression":63,"seo":1080,"stem":1081,"subtopic":1082,"tags":1083,"tldr":1088,"topic":1089,"triggered":154,"__hash__":1090},"lessons\u002Flessons\u002F2026-04-03.md","ripgrep's Hidden Power: Advanced Search Configuration",{"type":8,"value":9,"toc":1064},"minimark",[10,14,24,31,36,42,50,85,88,96,103,107,118,232,235,239,245,248,355,362,366,372,462,465,469,475,596,599,603,610,705,708,712,1057,1060],[11,12,6],"h1",{"id":13},"ripgreps-hidden-power-advanced-search-configuration",[15,16,17,18,23],"p",{},"Most developers know ripgrep as \"the fast grep replacement.\" ",[19,20,22],"cite",{"index":21},"3-9,3-11","Nearly every AI coding agent that needs to search a codebase reaches for the same binary: ripgrep. When milliseconds matter - and they absolutely matter when an LLM is waiting for context - the difference between grep and ripgrep is the difference between a responsive agent and one that feels broken."," But the real story isn't just speed—it's surgical precision through advanced configuration.",[15,25,26,30],{},[19,27,29],{"index":28},"3-25,3-26,3-28","Claude Code (Anthropic's CLI agent) uses ripgrep as one of its primary tools for understanding codebases. When you ask Claude Code to fix a bug or add a feature, it runs rg commands to find relevant code, trace dependencies, and understand call patterns. Claude Code often runs 10-30 ripgrep searches in a single task, finding function definitions, usages, imports, test files, and configuration."," This isn't luck—it's configuration mastery.",[32,33,35],"h2",{"id":34},"configuration-files-your-search-profile","Configuration Files: Your Search Profile",[15,37,38],{},[19,39,41],{"index":40},"6-9,6-10,6-11,6-12,6-13","ripgrep supports configuration files. Set RIPGREP_CONFIG_PATH to a configuration file. The file can specify one shell argument per line. Lines starting with # are ignored.",[15,43,44,45,49],{},"Create ",[46,47,48],"code",{},"~\u002F.ripgreprc"," and point to it:",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-bash shiki shiki-themes github-light","export RIPGREP_CONFIG_PATH=\"$HOME\u002F.ripgreprc\"\n","bash","",[46,58,59],{"__ignoreMap":56},[60,61,64,68,72,75,79,82],"span",{"class":62,"line":63},"line",1,[60,65,67],{"class":66},"sD7c4","export",[60,69,71],{"class":70},"sgsFI"," RIPGREP_CONFIG_PATH",[60,73,74],{"class":66},"=",[60,76,78],{"class":77},"sYBdl","\"",[60,80,81],{"class":70},"$HOME",[60,83,84],{"class":77},"\u002F.ripgreprc\"\n",[15,86,87],{},"Your config becomes your search personality:",[51,89,94],{"className":90,"code":92,"language":93},[91],"language-text","# Smart case by default\n--smart-case\n\n# Always show line numbers  \n--line-number\n\n# Add context around matches\n--context=2\n\n# Ignore these patterns globally\n--glob=!*.log\n--glob=!node_modules\u002F**\n--glob=!.git\u002F**\n--glob=!dist\u002F**\n--glob=!build\u002F**\n\n# Custom file types\n--type-add=proto:*.proto\n--type-add=config:*.{yml,yaml,toml,json,ini}\n--type-add=docs:*.{md,rst,txt,adoc}\n\n# Performance tweaks\n--mmap\n","text",[46,95,92],{"__ignoreMap":56},[15,97,98,99,102],{},"Now ",[46,100,101],{},"rg pattern"," runs with your entire profile applied. No more memorizing flags.",[32,104,106],{"id":105},"preprocessing-filters-search-beyond-text","Preprocessing Filters: Search Beyond Text",[15,108,109,113,114,117],{},[19,110,112],{"index":111},"2-1,2-13","This includes, but is not limited to, configuration files, passthru, support for searching compressed files, multiline search and opt-in fancy regex support via PCRE2."," The ",[46,115,116],{},"--pre"," flag transforms any file type before searching:",[51,119,121],{"className":53,"code":120,"language":55,"meta":56,"style":56},"# Search inside compressed files\nrg --pre 'zcat' 'error' logs\u002F\n\n# Search PDF content \nrg --pre 'pdftotext {} -' 'contract' documents\u002F\n\n# Search Docker images as tar streams\nrg --pre 'docker save {} | tar -xO' 'FROM ubuntu' .\n\n# Search minified JS after beautification\nrg --pre 'js-beautify' 'function.*login' assets\u002F\n",[46,122,123,129,149,156,162,178,183,189,205,210,216],{"__ignoreMap":56},[60,124,125],{"class":62,"line":63},[60,126,128],{"class":127},"sAwPA","# Search inside compressed files\n",[60,130,132,136,140,143,146],{"class":62,"line":131},2,[60,133,135],{"class":134},"s7eDp","rg",[60,137,139],{"class":138},"sYu0t"," --pre",[60,141,142],{"class":77}," 'zcat'",[60,144,145],{"class":77}," 'error'",[60,147,148],{"class":77}," logs\u002F\n",[60,150,152],{"class":62,"line":151},3,[60,153,155],{"emptyLinePlaceholder":154},true,"\n",[60,157,159],{"class":62,"line":158},4,[60,160,161],{"class":127},"# Search PDF content \n",[60,163,165,167,169,172,175],{"class":62,"line":164},5,[60,166,135],{"class":134},[60,168,139],{"class":138},[60,170,171],{"class":77}," 'pdftotext {} -'",[60,173,174],{"class":77}," 'contract'",[60,176,177],{"class":77}," documents\u002F\n",[60,179,181],{"class":62,"line":180},6,[60,182,155],{"emptyLinePlaceholder":154},[60,184,186],{"class":62,"line":185},7,[60,187,188],{"class":127},"# Search Docker images as tar streams\n",[60,190,192,194,196,199,202],{"class":62,"line":191},8,[60,193,135],{"class":134},[60,195,139],{"class":138},[60,197,198],{"class":77}," 'docker save {} | tar -xO'",[60,200,201],{"class":77}," 'FROM ubuntu'",[60,203,204],{"class":77}," .\n",[60,206,208],{"class":62,"line":207},9,[60,209,155],{"emptyLinePlaceholder":154},[60,211,213],{"class":62,"line":212},10,[60,214,215],{"class":127},"# Search minified JS after beautification\n",[60,217,219,221,223,226,229],{"class":62,"line":218},11,[60,220,135],{"class":134},[60,222,139],{"class":138},[60,224,225],{"class":77}," 'js-beautify'",[60,227,228],{"class":77}," 'function.*login'",[60,230,231],{"class":77}," assets\u002F\n",[15,233,234],{},"The preprocessing happens transparently. ripgrep streams the transformed content without touching your original files.",[32,236,238],{"id":237},"type-system-mastery","Type System Mastery",[15,240,241],{},[19,242,244],{"index":243},"2-8,2-9,2-10","ripgrep can search specific types of files. For example, rg -tpy foo limits your search to Python files and rg -Tjs foo excludes JavaScript files from your search. ripgrep can be taught about new file types with custom matching rules.",[15,246,247],{},"Define project-specific types:",[51,249,251],{"className":53,"code":250,"language":55,"meta":56,"style":56},"# Infrastructure files\nrg --type-add 'infra:*.{tf,tfvars,hcl}' --type infra 'resource.*aws_instance'\n\n# Frontend assets\nrg --type-add 'assets:*.{scss,less,styl,vue,svelte}' --type assets '@import'\n\n# Config files that might contain secrets\nrg --type-add 'secrets:*.{env,properties,conf,ini}' --type secrets 'password|key|token'\n\n# Multiple extensions, multiple patterns\nrg --type-add 'backend:*.{go,rs,py,rb,php}' --type backend 'func.*Handler|def.*handler|function.*handle'\n",[46,252,253,258,277,281,286,303,307,312,329,333,338],{"__ignoreMap":56},[60,254,255],{"class":62,"line":63},[60,256,257],{"class":127},"# Infrastructure files\n",[60,259,260,262,265,268,271,274],{"class":62,"line":131},[60,261,135],{"class":134},[60,263,264],{"class":138}," --type-add",[60,266,267],{"class":77}," 'infra:*.{tf,tfvars,hcl}'",[60,269,270],{"class":138}," --type",[60,272,273],{"class":77}," infra",[60,275,276],{"class":77}," 'resource.*aws_instance'\n",[60,278,279],{"class":62,"line":151},[60,280,155],{"emptyLinePlaceholder":154},[60,282,283],{"class":62,"line":158},[60,284,285],{"class":127},"# Frontend assets\n",[60,287,288,290,292,295,297,300],{"class":62,"line":164},[60,289,135],{"class":134},[60,291,264],{"class":138},[60,293,294],{"class":77}," 'assets:*.{scss,less,styl,vue,svelte}'",[60,296,270],{"class":138},[60,298,299],{"class":77}," assets",[60,301,302],{"class":77}," '@import'\n",[60,304,305],{"class":62,"line":180},[60,306,155],{"emptyLinePlaceholder":154},[60,308,309],{"class":62,"line":185},[60,310,311],{"class":127},"# Config files that might contain secrets\n",[60,313,314,316,318,321,323,326],{"class":62,"line":191},[60,315,135],{"class":134},[60,317,264],{"class":138},[60,319,320],{"class":77}," 'secrets:*.{env,properties,conf,ini}'",[60,322,270],{"class":138},[60,324,325],{"class":77}," secrets",[60,327,328],{"class":77}," 'password|key|token'\n",[60,330,331],{"class":62,"line":207},[60,332,155],{"emptyLinePlaceholder":154},[60,334,335],{"class":62,"line":212},[60,336,337],{"class":127},"# Multiple extensions, multiple patterns\n",[60,339,340,342,344,347,349,352],{"class":62,"line":218},[60,341,135],{"class":134},[60,343,264],{"class":138},[60,345,346],{"class":77}," 'backend:*.{go,rs,py,rb,php}'",[60,348,270],{"class":138},[60,350,351],{"class":77}," backend",[60,353,354],{"class":77}," 'func.*Handler|def.*handler|function.*handle'\n",[15,356,357,361],{},[19,358,360],{"index":359},"8-52,8-53,8-54","Search specific file types or define your own. Useful when: Built-in file type matching isn't enough."," The type system composes—combine multiple types, exclude unwanted ones, or build complex inclusion patterns.",[32,363,365],{"id":364},"context-control-for-code-analysis","Context Control for Code Analysis",[15,367,368],{},[19,369,371],{"index":370},"8-40,8-41,8-42","Show lines after, before, or around a match. Useful when: You want surrounding context, not just isolated matching lines. Makes your search results feel story-like instead of fragmented.",[51,373,375],{"className":53,"code":374,"language":55,"meta":56,"style":56},"# Function signatures with their bodies\nrg --after-context=10 'func.*Handler'\n\n# Import statements and what follows\nrg --before-context=2 --after-context=5 '^import'\n\n# Error handling patterns with context\nrg --context=3 'panic\\!|unwrap\\(\\)' --type rust\n\n# API endpoint definitions with middleware\nrg --context=8 'app\\.(get|post|put|delete)' --type js\n",[46,376,377,382,392,396,401,414,418,423,438,442,447],{"__ignoreMap":56},[60,378,379],{"class":62,"line":63},[60,380,381],{"class":127},"# Function signatures with their bodies\n",[60,383,384,386,389],{"class":62,"line":131},[60,385,135],{"class":134},[60,387,388],{"class":138}," --after-context=10",[60,390,391],{"class":77}," 'func.*Handler'\n",[60,393,394],{"class":62,"line":151},[60,395,155],{"emptyLinePlaceholder":154},[60,397,398],{"class":62,"line":158},[60,399,400],{"class":127},"# Import statements and what follows\n",[60,402,403,405,408,411],{"class":62,"line":164},[60,404,135],{"class":134},[60,406,407],{"class":138}," --before-context=2",[60,409,410],{"class":138}," --after-context=5",[60,412,413],{"class":77}," '^import'\n",[60,415,416],{"class":62,"line":180},[60,417,155],{"emptyLinePlaceholder":154},[60,419,420],{"class":62,"line":185},[60,421,422],{"class":127},"# Error handling patterns with context\n",[60,424,425,427,430,433,435],{"class":62,"line":191},[60,426,135],{"class":134},[60,428,429],{"class":138}," --context=3",[60,431,432],{"class":77}," 'panic\\!|unwrap\\(\\)'",[60,434,270],{"class":138},[60,436,437],{"class":77}," rust\n",[60,439,440],{"class":62,"line":207},[60,441,155],{"emptyLinePlaceholder":154},[60,443,444],{"class":62,"line":212},[60,445,446],{"class":127},"# API endpoint definitions with middleware\n",[60,448,449,451,454,457,459],{"class":62,"line":218},[60,450,135],{"class":134},[60,452,453],{"class":138}," --context=8",[60,455,456],{"class":77}," 'app\\.(get|post|put|delete)'",[60,458,270],{"class":138},[60,460,461],{"class":77}," js\n",[15,463,464],{},"Context isn't just pretty output—it's essential for understanding code relationships without opening files.",[32,466,468],{"id":467},"json-output-for-tool-integration","JSON Output for Tool Integration",[15,470,471],{},[19,472,474],{"index":473},"8-49,8-50,8-51","Output matches in JSON lines format. Useful when: You want machine-readable results (for piping into scripts, or UIs). VS Code search runs a variation of this to parse results live.",[51,476,478],{"className":53,"code":477,"language":55,"meta":56,"style":56},"# Pipe to jq for processing\nrg --json 'TODO' | jq -r 'select(.type==\"match\") | \"\\(.data.path.text):\\(.data.line_number): \\(.data.lines.text)\"'\n\n# Count matches per file type\nrg --json --type-all 'function' | jq -r 'select(.type==\"match\") | .data.path.text' | grep -o '\\.[^.]*$' | sort | uniq -c\n\n# Extract function names with metadata\nrg --json --only-matching 'function\\s+\\w+' --type js | jq -r '.data | \"\\(.path.text):\\(.line_number): \\(.lines.text)\"'\n",[46,479,480,485,507,511,516,561,565,570],{"__ignoreMap":56},[60,481,482],{"class":62,"line":63},[60,483,484],{"class":127},"# Pipe to jq for processing\n",[60,486,487,489,492,495,498,501,504],{"class":62,"line":131},[60,488,135],{"class":134},[60,490,491],{"class":138}," --json",[60,493,494],{"class":77}," 'TODO'",[60,496,497],{"class":66}," |",[60,499,500],{"class":134}," jq",[60,502,503],{"class":138}," -r",[60,505,506],{"class":77}," 'select(.type==\"match\") | \"\\(.data.path.text):\\(.data.line_number): \\(.data.lines.text)\"'\n",[60,508,509],{"class":62,"line":151},[60,510,155],{"emptyLinePlaceholder":154},[60,512,513],{"class":62,"line":158},[60,514,515],{"class":127},"# Count matches per file type\n",[60,517,518,520,522,525,528,530,532,534,537,539,542,545,548,550,553,555,558],{"class":62,"line":164},[60,519,135],{"class":134},[60,521,491],{"class":138},[60,523,524],{"class":138}," --type-all",[60,526,527],{"class":77}," 'function'",[60,529,497],{"class":66},[60,531,500],{"class":134},[60,533,503],{"class":138},[60,535,536],{"class":77}," 'select(.type==\"match\") | .data.path.text'",[60,538,497],{"class":66},[60,540,541],{"class":134}," grep",[60,543,544],{"class":138}," -o",[60,546,547],{"class":77}," '\\.[^.]*$'",[60,549,497],{"class":66},[60,551,552],{"class":134}," sort",[60,554,497],{"class":66},[60,556,557],{"class":134}," uniq",[60,559,560],{"class":138}," -c\n",[60,562,563],{"class":62,"line":180},[60,564,155],{"emptyLinePlaceholder":154},[60,566,567],{"class":62,"line":185},[60,568,569],{"class":127},"# Extract function names with metadata\n",[60,571,572,574,576,579,582,584,587,589,591,593],{"class":62,"line":191},[60,573,135],{"class":134},[60,575,491],{"class":138},[60,577,578],{"class":138}," --only-matching",[60,580,581],{"class":77}," 'function\\s+\\w+'",[60,583,270],{"class":138},[60,585,586],{"class":77}," js",[60,588,497],{"class":66},[60,590,500],{"class":134},[60,592,503],{"class":138},[60,594,595],{"class":77}," '.data | \"\\(.path.text):\\(.line_number): \\(.lines.text)\"'\n",[15,597,598],{},"JSON mode transforms ripgrep into a structured data extraction engine.",[32,600,602],{"id":601},"pro-tip","Pro Tip",[15,604,605,609],{},[19,606,608],{"index":607},"1-3,1-15","Shell completions (and man page) can be created via rg --generate."," Generate completions for your shell:",[51,611,613],{"className":53,"code":612,"language":55,"meta":56,"style":56},"# Bash\nrg --generate complete-bash > ~\u002F.local\u002Fshare\u002Fbash-completion\u002Fcompletions\u002Frg\n\n# Zsh  \nrg --generate complete-zsh > ~\u002F.zfunc\u002F_rg\n\n# Fish\nrg --generate complete-fish > ~\u002F.config\u002Ffish\u002Fcompletions\u002Frg.fish\n\n# PowerShell\nrg --generate complete-powershell > rg.ps1\n",[46,614,615,620,636,640,645,659,663,668,682,686,691],{"__ignoreMap":56},[60,616,617],{"class":62,"line":63},[60,618,619],{"class":127},"# Bash\n",[60,621,622,624,627,630,633],{"class":62,"line":131},[60,623,135],{"class":134},[60,625,626],{"class":138}," --generate",[60,628,629],{"class":77}," complete-bash",[60,631,632],{"class":66}," >",[60,634,635],{"class":77}," ~\u002F.local\u002Fshare\u002Fbash-completion\u002Fcompletions\u002Frg\n",[60,637,638],{"class":62,"line":151},[60,639,155],{"emptyLinePlaceholder":154},[60,641,642],{"class":62,"line":158},[60,643,644],{"class":127},"# Zsh  \n",[60,646,647,649,651,654,656],{"class":62,"line":164},[60,648,135],{"class":134},[60,650,626],{"class":138},[60,652,653],{"class":77}," complete-zsh",[60,655,632],{"class":66},[60,657,658],{"class":77}," ~\u002F.zfunc\u002F_rg\n",[60,660,661],{"class":62,"line":180},[60,662,155],{"emptyLinePlaceholder":154},[60,664,665],{"class":62,"line":185},[60,666,667],{"class":127},"# Fish\n",[60,669,670,672,674,677,679],{"class":62,"line":191},[60,671,135],{"class":134},[60,673,626],{"class":138},[60,675,676],{"class":77}," complete-fish",[60,678,632],{"class":66},[60,680,681],{"class":77}," ~\u002F.config\u002Ffish\u002Fcompletions\u002Frg.fish\n",[60,683,684],{"class":62,"line":207},[60,685,155],{"emptyLinePlaceholder":154},[60,687,688],{"class":62,"line":212},[60,689,690],{"class":127},"# PowerShell\n",[60,692,693,695,697,700,702],{"class":62,"line":218},[60,694,135],{"class":134},[60,696,626],{"class":138},[60,698,699],{"class":77}," complete-powershell",[60,701,632],{"class":66},[60,703,704],{"class":77}," rg.ps1\n",[15,706,707],{},"With completions installed, shell tab-completion works for all ripgrep flags, file types, and glob patterns.",[32,709,711],{"id":710},"example-multi-stage-security-audit","Example: Multi-Stage Security Audit",[51,713,715],{"className":53,"code":714,"language":55,"meta":56,"style":56},"#!\u002Fbin\u002Fbash\n# security-audit.sh - Comprehensive codebase security scan\n\n# 1. Find potential secrets\necho \"=== Potential Secrets ===\"\nrg --type-add 'secrets:*.{env,properties,yaml,json,py,js,go,rs}' \\\n   --type secrets \\\n   --ignore-case \\\n   'password|secret|key|token|api_key' \\\n   --context=1 \\\n   --json | jq -r 'select(.type==\"match\") | \"\\(.data.path.text):\\(.data.line_number)\"'\n\n# 2. Check for hardcoded credentials patterns\necho \"=== Hardcoded Credentials ===\"\nrg --type-add 'code:*.{py,js,go,rs,java,php,rb}' \\\n   --type code \\\n   --pcre2 \\\n   '(?i)(password|secret|key)\\s*[=:]\\s*[\"\\'][^\"\\'\n]{8,}[\"\\']' \\\n   --only-matching \\\n   --line-number\n\n# 3. Find SQL injection risks\necho \"=== SQL Injection Risks ===\"\nrg --type-add 'web:*.{py,js,php,rb,java}' \\\n   --type web \\\n   --multiline \\\n   --pcre2 \\\n   '(?s)(query|execute).*\\+.*\\$\\{|\\$[a-zA-Z]' \\\n   --context=2\n\n# 4. Look for debug\u002Fdevelopment code\necho \"=== Debug Code ===\"\nrg --type-add 'all-code:*.{py,js,go,rs,java,php,rb,cpp,c}' \\\n   --type all-code \\\n   'console\\.log|print\\(|debug|TODO.*SECURITY|FIXME.*AUTH' \\\n   --line-number\n",[46,716,717,722,727,731,736,744,756,765,772,779,786,800,805,811,819,831,841,849,855,867,873,879,884,890,896,908,914,920,926,976,982,987,993,999,1013,1019,1052],{"__ignoreMap":56},[60,718,719],{"class":62,"line":63},[60,720,721],{"class":127},"#!\u002Fbin\u002Fbash\n",[60,723,724],{"class":62,"line":131},[60,725,726],{"class":127},"# security-audit.sh - Comprehensive codebase security scan\n",[60,728,729],{"class":62,"line":151},[60,730,155],{"emptyLinePlaceholder":154},[60,732,733],{"class":62,"line":158},[60,734,735],{"class":127},"# 1. Find potential secrets\n",[60,737,738,741],{"class":62,"line":164},[60,739,740],{"class":138},"echo",[60,742,743],{"class":77}," \"=== Potential Secrets ===\"\n",[60,745,746,748,750,753],{"class":62,"line":180},[60,747,135],{"class":134},[60,749,264],{"class":138},[60,751,752],{"class":77}," 'secrets:*.{env,properties,yaml,json,py,js,go,rs}'",[60,754,755],{"class":138}," \\\n",[60,757,758,761,763],{"class":62,"line":185},[60,759,760],{"class":138},"   --type",[60,762,325],{"class":77},[60,764,755],{"class":138},[60,766,767,770],{"class":62,"line":191},[60,768,769],{"class":138},"   --ignore-case",[60,771,755],{"class":138},[60,773,774,777],{"class":62,"line":207},[60,775,776],{"class":77},"   'password|secret|key|token|api_key'",[60,778,755],{"class":138},[60,780,781,784],{"class":62,"line":212},[60,782,783],{"class":138},"   --context=1",[60,785,755],{"class":138},[60,787,788,791,793,795,797],{"class":62,"line":218},[60,789,790],{"class":138},"   --json",[60,792,497],{"class":66},[60,794,500],{"class":134},[60,796,503],{"class":138},[60,798,799],{"class":77}," 'select(.type==\"match\") | \"\\(.data.path.text):\\(.data.line_number)\"'\n",[60,801,803],{"class":62,"line":802},12,[60,804,155],{"emptyLinePlaceholder":154},[60,806,808],{"class":62,"line":807},13,[60,809,810],{"class":127},"# 2. Check for hardcoded credentials patterns\n",[60,812,814,816],{"class":62,"line":813},14,[60,815,740],{"class":138},[60,817,818],{"class":77}," \"=== Hardcoded Credentials ===\"\n",[60,820,822,824,826,829],{"class":62,"line":821},15,[60,823,135],{"class":134},[60,825,264],{"class":138},[60,827,828],{"class":77}," 'code:*.{py,js,go,rs,java,php,rb}'",[60,830,755],{"class":138},[60,832,834,836,839],{"class":62,"line":833},16,[60,835,760],{"class":138},[60,837,838],{"class":77}," code",[60,840,755],{"class":138},[60,842,844,847],{"class":62,"line":843},17,[60,845,846],{"class":138},"   --pcre2",[60,848,755],{"class":138},[60,850,852],{"class":62,"line":851},18,[60,853,854],{"class":77},"   '(?i)(password|secret|key)\\s*[=:]\\s*[\"\\'][^\"\\'\n",[60,856,858,861,864],{"class":62,"line":857},19,[60,859,860],{"class":77},"]{8,}[\"",[60,862,863],{"class":138},"\\'",[60,865,866],{"class":77},"]' \\\n",[60,868,870],{"class":62,"line":869},20,[60,871,872],{"class":77},"   --only-matching \\\n",[60,874,876],{"class":62,"line":875},21,[60,877,878],{"class":77},"   --line-number\n",[60,880,882],{"class":62,"line":881},22,[60,883,155],{"emptyLinePlaceholder":154},[60,885,887],{"class":62,"line":886},23,[60,888,889],{"class":77},"# 3. Find SQL injection risks\n",[60,891,893],{"class":62,"line":892},24,[60,894,895],{"class":77},"echo \"=== SQL Injection Risks ===\"\n",[60,897,899,902,905],{"class":62,"line":898},25,[60,900,901],{"class":77},"rg --type-add 'web:",[60,903,904],{"class":138},"*",[60,906,907],{"class":77},".{py,js,php,rb,java}' \\\n",[60,909,911],{"class":62,"line":910},26,[60,912,913],{"class":77},"   --type web \\\n",[60,915,917],{"class":62,"line":916},27,[60,918,919],{"class":77},"   --multiline \\\n",[60,921,923],{"class":62,"line":922},28,[60,924,925],{"class":77},"   --pcre2 \\\n",[60,927,929,932,935,938,941,944,947,950,953,956,958,961,963,965,968,970,973],{"class":62,"line":928},29,[60,930,931],{"class":77},"   '",[60,933,934],{"class":70},"(",[60,936,937],{"class":66},"?",[60,939,940],{"class":70},"s)(",[60,942,943],{"class":134},"query",[60,945,946],{"class":66},"|",[60,948,949],{"class":134},"execute",[60,951,952],{"class":70},")",[60,954,955],{"class":77},".",[60,957,904],{"class":138},[60,959,960],{"class":138},"\\+",[60,962,955],{"class":77},[60,964,904],{"class":138},[60,966,967],{"class":138},"\\$\\{",[60,969,946],{"class":66},[60,971,972],{"class":134},"\\$[a-zA-Z]",[60,974,975],{"class":134},"' \\\n",[60,977,979],{"class":62,"line":978},30,[60,980,981],{"class":134},"   --context=2\n",[60,983,985],{"class":62,"line":984},31,[60,986,155],{"emptyLinePlaceholder":154},[60,988,990],{"class":62,"line":989},32,[60,991,992],{"class":134},"# 4. Look for debug\u002Fdevelopment code\n",[60,994,996],{"class":62,"line":995},33,[60,997,998],{"class":134},"echo \"=== Debug Code ===\"\n",[60,1000,1002,1005,1008,1011],{"class":62,"line":1001},34,[60,1003,1004],{"class":134},"rg --type-add '",[60,1006,1007],{"class":134},"all-code:*.",[60,1009,1010],{"class":77},"{py,js,go,rs,java,php,rb,cpp,c}",[60,1012,975],{"class":134},[60,1014,1016],{"class":62,"line":1015},35,[60,1017,1018],{"class":134},"   --type all-code \\\n",[60,1020,1022,1024,1027,1029,1032,1035,1037,1040,1042,1045,1047,1050],{"class":62,"line":1021},36,[60,1023,931],{"class":134},[60,1025,1026],{"class":134},"console\\.log",[60,1028,946],{"class":66},[60,1030,1031],{"class":138},"print",[60,1033,1034],{"class":70},"\\(",[60,1036,946],{"class":66},[60,1038,1039],{"class":134},"debug",[60,1041,946],{"class":66},[60,1043,1044],{"class":134},"TODO.*SECURITY",[60,1046,946],{"class":66},[60,1048,1049],{"class":134},"FIXME.*AUTH",[60,1051,975],{"class":134},[60,1053,1055],{"class":62,"line":1054},37,[60,1056,878],{"class":134},[15,1058,1059],{},"This script demonstrates ripgrep's power as a security analysis engine—not just a search tool, but a configurable code inspection platform. The combination of custom types, preprocessing, regex engines, and output formats transforms simple text search into sophisticated codebase analysis.",[1061,1062,1063],"style",{},"html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}",{"title":56,"searchDepth":131,"depth":131,"links":1065},[1066,1067,1068,1069,1070,1071,1072],{"id":34,"depth":131,"text":35},{"id":105,"depth":131,"text":106},{"id":237,"depth":131,"text":238},{"id":364,"depth":131,"text":365},{"id":467,"depth":131,"text":468},{"id":601,"depth":131,"text":602},{"id":710,"depth":131,"text":711},"2026-04-03","Most developers know ripgrep as \"the fast grep replacement.\" Nearly every AI coding agent that needs to search a codebase reaches for the same binary: ripgrep. When milliseconds matter - and they absolutely matter when an LLM is waiting for context - the difference between grep and ripgrep is the difference between a responsive agent and one that feels broken. But the real story isn't just speed—it's surgical precision through advanced configuration.","intermediate","md","deep-dive",{},"\u002Flessons\u002F2026-04-03",{"title":6,"description":1074},"lessons\u002F2026-04-03","ripgrep-advanced",[1084,1085,1086,1087],"ripgrep","search","performance","configuration","Beyond basic searching, ripgrep's configuration system, preprocessing filters, and specialized flags transform it from fast grep into a surgical code analysis engine.","tools","0so1PENQ8wwg9Znu7zEpj_6Ur0OfIrWjucnemuCdfeQ",{"id":1092,"title":1093,"body":1094,"date":1468,"description":1469,"difficulty":1075,"extension":1076,"format":1077,"meta":1470,"navigation":154,"path":1471,"progression":151,"seo":1472,"stem":1473,"subtopic":1474,"tags":1475,"tldr":1480,"topic":1133,"triggered":154,"__hash__":1481},"lessons\u002Flessons\u002F2026-04-02.md","Macro Composition: Building Complex Vim Automations",{"type":8,"value":1095,"toc":1458},[1096,1099,1114,1122,1126,1129,1140,1151,1158,1162,1197,1200,1206,1209,1233,1237,1243,1246,1261,1273,1277,1283,1286,1306,1309,1318,1322,1328,1331,1340,1343,1347,1353,1359,1367,1382,1384,1391,1395,1452,1455],[11,1097,1093],{"id":1098},"macro-composition-building-complex-vim-automations",[15,1100,1101,1102,1105,1106,1109,1110,955],{},"Most Vim users record macros the traditional way: ",[46,1103,1104],{},"qa",", fumble through some commands, hope nothing breaks, then ",[46,1107,1108],{},"q",". But ",[19,1111,1113],{"index":1112},"9-21,9-22,9-23","you shouldn't compose macros on the fly — if a macro is more than three keystrokes, compose it directly in a scratch buffer",[15,1115,1116,1117,1121],{},"Macros are text. Once you internalize this, everything changes. ",[19,1118,1120],{"index":1119},"9-1,9-13","Running a macro is simply instructing Vim to take the contents of a register and execute them as keystrokes",". This means you can write, edit, debug, and version-control your macros like any other code.",[32,1123,1125],{"id":1124},"constructing-macros-as-text","Constructing Macros as Text",[15,1127,1128],{},"Instead of recording, build macros by setting register contents directly:",[51,1130,1134],{"className":1131,"code":1132,"language":1133,"meta":56,"style":56},"language-vim shiki shiki-themes github-light",":let @q = 'I\u002F\u002F ^[A (added)^[j'\n","vim",[46,1135,1136],{"__ignoreMap":56},[60,1137,1138],{"class":62,"line":63},[60,1139,1132],{},[15,1141,1142,1143,1146,1147,955],{},"The ",[46,1144,1145],{},"^["," represents the Escape key. ",[19,1148,1150],{"index":1149},"8-21,8-22,8-23","To insert special keys like Escape and Enter, use Ctrl+v followed by the key — you'll get ^[ for Escape, ^M for Enter",[15,1152,1153,1154,955],{},"Better yet, use single quotes to avoid escaping nightmares. ",[19,1155,1157],{"index":1156},"7-22,7-23","Single quotes allow you to type almost everything literally, whereas double-quotes require escaping characters like backslashes and quotes",[32,1159,1161],{"id":1160},"the-iterative-macro-development-process","The Iterative Macro Development Process",[1163,1164,1165,1173,1182,1188],"ol",{},[1166,1167,1168,1172],"li",{},[1169,1170,1171],"strong",{},"Write the macro in a scratch buffer"," — open a new buffer and type out your intended keystrokes",[1166,1174,1175,1178,1179],{},[1169,1176,1177],{},"Test incrementally"," — select portions and execute with ",[46,1180,1181],{},"@\"",[1166,1183,1184,1187],{},[1169,1185,1186],{},"Refine and debug"," — edit the text directly, no re-recording required",[1166,1189,1190,1193,1194],{},[1169,1191,1192],{},"Load into a register"," — ",[46,1195,1196],{},":let @q = 'your-macro-text'",[15,1198,1199],{},"Consider this macro for wrapping function calls with error handling:",[51,1201,1204],{"className":1202,"code":1203,"language":93},[91],"ciw^Rtry { ^[pA } catch(e) { console.error(e); }^[f{%\n",[46,1205,1203],{"__ignoreMap":56},[15,1207,1208],{},"Breaking it down:",[1210,1211,1212,1218,1224,1227],"ul",{},[1166,1213,1214,1217],{},[46,1215,1216],{},"ciw"," — change inner word (replace function name)",[1166,1219,1220,1223],{},[46,1221,1222],{},"^R"," — paste from register",[1166,1225,1226],{},"Build the try-catch structure",[1166,1228,1229,1232],{},[46,1230,1231],{},"f{%"," — jump to opening brace, then to matching closing brace",[32,1234,1236],{"id":1235},"macro-editing-and-repair","Macro Editing and Repair",[15,1238,1239,955],{},[19,1240,1242],{"index":1241},"8-30,8-31,8-32,8-33","Registers are shared across recording, delete and yank commands. When you call a macro, the register content is treated as commands. So editing a register automatically updates the macro behavior",[15,1244,1245],{},"To inspect macro content:",[51,1247,1249],{"className":1131,"code":1248,"language":1133,"meta":56,"style":56},":reg q    \" View register q contents\n\"qp       \" Paste macro content to edit\n",[46,1250,1251,1256],{"__ignoreMap":56},[60,1252,1253],{"class":62,"line":63},[60,1254,1255],{},":reg q    \" View register q contents\n",[60,1257,1258],{"class":62,"line":131},[60,1259,1260],{},"\"qp       \" Paste macro content to edit\n",[15,1262,1263,1264,1267,1268,1272],{},"After editing, reload with ",[46,1265,1266],{},":let @q = 'new-content'",". ",[19,1269,1271],{"index":1270},"1-17","Macros are just text stored in named registers",", so treat them like any other text object.",[32,1274,1276],{"id":1275},"combining-macros-with-ex-commands","Combining Macros with Ex Commands",[15,1278,1279,955],{},[19,1280,1282],{"index":1281},"9-15,9-16","You needn't restrict yourself to single-keystroke vi commands when composing macros. You can include Ex commands as well",[15,1284,1285],{},"Powerful pattern: macro + range operation:",[51,1287,1289],{"className":1131,"code":1288,"language":1133,"meta":56,"style":56},":let @f = 'I    ^[>>j'                    \" Add 4 spaces and indent\n:15,25norm @f                            \" Apply to lines 15-25\n:%norm @f                                \" Apply to entire file\n",[46,1290,1291,1296,1301],{"__ignoreMap":56},[60,1292,1293],{"class":62,"line":63},[60,1294,1295],{},":let @f = 'I    ^[>>j'                    \" Add 4 spaces and indent\n",[60,1297,1298],{"class":62,"line":131},[60,1299,1300],{},":15,25norm @f                            \" Apply to lines 15-25\n",[60,1302,1303],{"class":62,"line":151},[60,1304,1305],{},":%norm @f                                \" Apply to entire file\n",[15,1307,1308],{},"Or embed substitutions within macros:",[51,1310,1312],{"className":1131,"code":1311,"language":1133,"meta":56,"style":56},":let @c = ':s\u002Ffunction\u002Fasync function\u002F^Mj'\n",[46,1313,1314],{"__ignoreMap":56},[60,1315,1316],{"class":62,"line":63},[60,1317,1311],{},[32,1319,1321],{"id":1320},"recursive-macros-for-complex-patterns","Recursive Macros for Complex Patterns",[15,1323,1324,955],{},[19,1325,1327],{"index":1326},"10-27,10-28","Recursion occurs when we execute a macro while recording it. Clear the register first with qqq",[15,1329,1330],{},"Classic recursive macro pattern — process until end of file:",[51,1332,1334],{"className":1131,"code":1333,"language":1133,"meta":56,"style":56},":let @r = '0\u002Fpattern^M\u003Coperations>j@r'\n",[46,1335,1336],{"__ignoreMap":56},[60,1337,1338],{"class":62,"line":63},[60,1339,1333],{},[15,1341,1342],{},"The recursion breaks when the search fails (no more matches), making it self-terminating.",[32,1344,1346],{"id":1345},"pro-tips-for-macro-composition","Pro Tips for Macro Composition",[15,1348,1349,955],{},[19,1350,1352],{"index":1351},"10-11,10-12,10-13","Use mnemonics to remember macros — if you have a macro for modifying functions use f register (qf), n (qn) for numbers. Name it with whatever first thing comes to mind",[15,1354,1355,955],{},[19,1356,1358],{"index":1357},"9-10,9-11","Don't forget you can prepend a count — 6@a would run the macro 6 times. Compose macros so they make sense when run multiple times",[15,1360,1361,1362,1366],{},"For persistent macros, ",[19,1363,1365],{"index":1364},"7-8,7-9","Vim automatically saves registers to viminfo and restores them at startup",". Or explicitly save to vimrc:",[51,1368,1370],{"className":1131,"code":1369,"language":1133,"meta":56,"style":56},"\" Permanent macro for CSS property formatting\nlet @c = 'I  ^[A;^[j'\n",[46,1371,1372,1377],{"__ignoreMap":56},[60,1373,1374],{"class":62,"line":63},[60,1375,1376],{},"\" Permanent macro for CSS property formatting\n",[60,1378,1379],{"class":62,"line":131},[60,1380,1381],{},"let @c = 'I  ^[A;^[j'\n",[32,1383,602],{"id":601},[15,1385,1386,1390],{},[19,1387,1389],{"index":1388},"7-29,7-30,7-31","To append to an existing macro instead of re-recording, use a capital letter. If you recorded with qa...q, append with qA...q",". This lets you iteratively build complex macros without losing your work.",[32,1392,1394],{"id":1393},"example","Example",[51,1396,1398],{"className":1131,"code":1397,"language":1133,"meta":56,"style":56},"\" Convert array destructuring to object destructuring\n:let @d = 'f[ci[{^[f]s}^['\n\n\" Before: const [a, b, c] = getData()\n\" After:  const {a, b, c} = getData()\n\n\" Apply to multiple lines:\n:10,20norm @d\n\n\" Or make it recursive to handle nested structures:\n:let @D = 'f[ci[{^[f]s}^[j@D'\n",[46,1399,1400,1405,1410,1414,1419,1424,1428,1433,1438,1442,1447],{"__ignoreMap":56},[60,1401,1402],{"class":62,"line":63},[60,1403,1404],{},"\" Convert array destructuring to object destructuring\n",[60,1406,1407],{"class":62,"line":131},[60,1408,1409],{},":let @d = 'f[ci[{^[f]s}^['\n",[60,1411,1412],{"class":62,"line":151},[60,1413,155],{"emptyLinePlaceholder":154},[60,1415,1416],{"class":62,"line":158},[60,1417,1418],{},"\" Before: const [a, b, c] = getData()\n",[60,1420,1421],{"class":62,"line":164},[60,1422,1423],{},"\" After:  const {a, b, c} = getData()\n",[60,1425,1426],{"class":62,"line":180},[60,1427,155],{"emptyLinePlaceholder":154},[60,1429,1430],{"class":62,"line":185},[60,1431,1432],{},"\" Apply to multiple lines:\n",[60,1434,1435],{"class":62,"line":191},[60,1436,1437],{},":10,20norm @d\n",[60,1439,1440],{"class":62,"line":207},[60,1441,155],{"emptyLinePlaceholder":154},[60,1443,1444],{"class":62,"line":212},[60,1445,1446],{},"\" Or make it recursive to handle nested structures:\n",[60,1448,1449],{"class":62,"line":218},[60,1450,1451],{},":let @D = 'f[ci[{^[f]s}^[j@D'\n",[15,1453,1454],{},"Macros aren't magic spells you record once and hope work. They're code. Write them, debug them, version them. Your future self will thank you when that 47-keystroke transformation just works.",[1061,1456,1457],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":56,"searchDepth":131,"depth":131,"links":1459},[1460,1461,1462,1463,1464,1465,1466,1467],{"id":1124,"depth":131,"text":1125},{"id":1160,"depth":131,"text":1161},{"id":1235,"depth":131,"text":1236},{"id":1275,"depth":131,"text":1276},{"id":1320,"depth":131,"text":1321},{"id":1345,"depth":131,"text":1346},{"id":601,"depth":131,"text":602},{"id":1393,"depth":131,"text":1394},"2026-04-02","Most Vim users record macros the traditional way: qa, fumble through some commands, hope nothing breaks, then q. But you shouldn't compose macros on the fly — if a macro is more than three keystrokes, compose it directly in a scratch buffer.",{},"\u002Flessons\u002F2026-04-02",{"title":1093,"description":1469},"lessons\u002F2026-04-02","macro-composition",[1133,1476,1477,1478,1479],"macros","automation","registers","productivity","Learn advanced macro techniques: building macros from scratch, editing macro content directly, combining macros with Ex commands, and creating recursive patterns for complex text transformations.","F1EJl1Zwfu1akYYDWdX5piM-RA4BCUd9920It2NjpLY",{"id":1483,"title":1484,"body":1485,"date":2082,"description":2083,"difficulty":1075,"extension":1076,"format":1077,"meta":2084,"navigation":154,"path":2085,"progression":131,"seo":2086,"stem":2087,"subtopic":2088,"tags":2089,"tldr":2094,"topic":2095,"triggered":2096,"__hash__":2097},"lessons\u002Flessons\u002F2026-04-01.md","Docker Multi-Stage Builds: Production-Grade Container Optimization",{"type":8,"value":1486,"toc":2069},[1487,1490,1497,1503,1507,1513,1516,1523,1527,1533,1539,1543,1548,1642,1653,1657,1806,1811,1815,1821,1828,1833,1848,1853,1877,1881,1888,1894,1901,1905,1911,1914,1916,1922,1929,1953,1955,1958,2064,2067],[11,1488,1484],{"id":1489},"docker-multi-stage-builds-production-grade-container-optimization",[15,1491,1492,1496],{},[19,1493,1495],{"index":1494},"2-1,3-4","A typical Node.js Docker image built the naive way can be over 1 GB. The same application built with multi-stage builds can be under 100 MB."," This isn't just about saving disk space—it's about fundamentally rethinking how we build production containers.",[15,1498,1499],{},[19,1500,1502],{"index":1501},"1-12,1-13,1-14,1-15","Multi-stage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain. With multi-stage builds, you use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don't want in the final image.",[32,1504,1506],{"id":1505},"the-problem-build-vs-runtime-requirements","The Problem: Build vs Runtime Requirements",[15,1508,1509],{},[19,1510,1512],{"index":1511},"8-6,8-7,8-8","Building and running apps are two completely separate problems with different sets of requirements and constraints. So, the build and runtime images should also be completely separate! Nevertheless, the need for such a separation is often overlooked, and production images end up having linters, compilers, and other dev tools in them.",[15,1514,1515],{},"Consider a Go application. Your build environment needs the full Go compiler toolchain, possibly CGO dependencies, build tools like make, and development utilities. Your production environment needs exactly one thing: the compiled binary. Yet traditional single-stage builds bundle everything together.",[15,1517,1518,1522],{},[19,1519,1521],{"index":1520},"8-25","The golang:1.23 brings more than 800MB of packages and about the same number of CVEs","—none of which your production application actually needs.",[32,1524,1526],{"id":1525},"multi-stage-architecture","Multi-Stage Architecture",[15,1528,1529],{},[19,1530,1532],{"index":1531},"7-21,7-22,7-23","Conceptually, think of this as a relay race. The first stage (the builder) runs the heavy lifting, grabs the baton (the compiled artifact), and passes it to the second stage (the runner). The first stage—and all its heavy layers—is then discarded, leaving only the lean final image.",[15,1534,1535],{},[19,1536,1538],{"index":1537},"7-24,7-25,7-27","The magic of multi-stage builds lies in two specific syntax capabilities: aliasing build stages and copying artifacts between them. Instead of a standard FROM image, we use FROM image AS alias. In the final stage, we use COPY --from=alias to selectively pull files from the previous stage's filesystem into the new one.",[32,1540,1542],{"id":1541},"language-specific-patterns","Language-Specific Patterns",[1544,1545,1547],"h3",{"id":1546},"go-from-gigabytes-to-megabytes","Go: From Gigabytes to Megabytes",[51,1549,1553],{"className":1550,"code":1551,"language":1552,"meta":56,"style":56},"language-dockerfile shiki shiki-themes github-light","# Build stage\nFROM golang:1.22-alpine AS builder\nWORKDIR \u002Fapp\n\n# Copy dependency files first for better caching\nCOPY go.mod go.sum .\u002F\nRUN go mod download\n\n# Copy source and build\nCOPY . .\nRUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .\n\n# Production stage\nFROM scratch\nCOPY --from=builder \u002Fapp\u002Fapp \u002Fapp\nCOPY --from=builder \u002Fetc\u002Fssl\u002Fcerts\u002Fca-certificates.crt \u002Fetc\u002Fssl\u002Fcerts\u002F\nEXPOSE 8080\nCMD [\"\u002Fapp\"]\n","dockerfile",[46,1554,1555,1560,1565,1570,1574,1579,1584,1589,1593,1598,1603,1608,1612,1617,1622,1627,1632,1637],{"__ignoreMap":56},[60,1556,1557],{"class":62,"line":63},[60,1558,1559],{},"# Build stage\n",[60,1561,1562],{"class":62,"line":131},[60,1563,1564],{},"FROM golang:1.22-alpine AS builder\n",[60,1566,1567],{"class":62,"line":151},[60,1568,1569],{},"WORKDIR \u002Fapp\n",[60,1571,1572],{"class":62,"line":158},[60,1573,155],{"emptyLinePlaceholder":154},[60,1575,1576],{"class":62,"line":164},[60,1577,1578],{},"# Copy dependency files first for better caching\n",[60,1580,1581],{"class":62,"line":180},[60,1582,1583],{},"COPY go.mod go.sum .\u002F\n",[60,1585,1586],{"class":62,"line":185},[60,1587,1588],{},"RUN go mod download\n",[60,1590,1591],{"class":62,"line":191},[60,1592,155],{"emptyLinePlaceholder":154},[60,1594,1595],{"class":62,"line":207},[60,1596,1597],{},"# Copy source and build\n",[60,1599,1600],{"class":62,"line":212},[60,1601,1602],{},"COPY . .\n",[60,1604,1605],{"class":62,"line":218},[60,1606,1607],{},"RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .\n",[60,1609,1610],{"class":62,"line":802},[60,1611,155],{"emptyLinePlaceholder":154},[60,1613,1614],{"class":62,"line":807},[60,1615,1616],{},"# Production stage\n",[60,1618,1619],{"class":62,"line":813},[60,1620,1621],{},"FROM scratch\n",[60,1623,1624],{"class":62,"line":821},[60,1625,1626],{},"COPY --from=builder \u002Fapp\u002Fapp \u002Fapp\n",[60,1628,1629],{"class":62,"line":833},[60,1630,1631],{},"COPY --from=builder \u002Fetc\u002Fssl\u002Fcerts\u002Fca-certificates.crt \u002Fetc\u002Fssl\u002Fcerts\u002F\n",[60,1633,1634],{"class":62,"line":843},[60,1635,1636],{},"EXPOSE 8080\n",[60,1638,1639],{"class":62,"line":851},[60,1640,1641],{},"CMD [\"\u002Fapp\"]\n",[15,1643,1644,1648,1649,1652],{},[19,1645,1647],{"index":1646},"2-4","Go application: Single stage (golang:1.22): ~1.1GB, Multi-stage (golang -> scratch): ~8MB (99% reduction)",". The ",[46,1650,1651],{},"scratch"," base image is literally empty—zero bytes—perfect for static binaries.",[1544,1654,1656],{"id":1655},"nodejs-eliminating-devdependencies","Node.js: Eliminating devDependencies",[51,1658,1660],{"className":1550,"code":1659,"language":1552,"meta":56,"style":56},"# Dependencies stage\nFROM node:20-alpine AS deps\nWORKDIR \u002Fapp\nCOPY package*.json .\u002F\nRUN npm ci --only=production\n\n# Build stage  \nFROM node:20-alpine AS builder\nWORKDIR \u002Fapp\nCOPY package*.json .\u002F\nRUN npm ci\nCOPY . .\nRUN npm run build\n\n# Production stage\nFROM node:20-alpine AS production\nWORKDIR \u002Fapp\nENV NODE_ENV=production\n\n# Create non-root user\nRUN addgroup -S appgroup && adduser -S appuser -G appgroup\n\n# Copy only production dependencies\nCOPY --from=deps \u002Fapp\u002Fnode_modules .\u002Fnode_modules\n# Copy built application\nCOPY --from=builder \u002Fapp\u002Fdist .\u002Fdist\nCOPY --from=builder \u002Fapp\u002Fpackage.json .\u002F\n\nUSER appuser\nEXPOSE 3000\nCMD [\"node\", \"dist\u002Findex.js\"]\n",[46,1661,1662,1667,1672,1676,1681,1686,1690,1695,1700,1704,1708,1713,1717,1722,1726,1730,1735,1739,1744,1748,1753,1758,1762,1767,1772,1777,1782,1787,1791,1796,1801],{"__ignoreMap":56},[60,1663,1664],{"class":62,"line":63},[60,1665,1666],{},"# Dependencies stage\n",[60,1668,1669],{"class":62,"line":131},[60,1670,1671],{},"FROM node:20-alpine AS deps\n",[60,1673,1674],{"class":62,"line":151},[60,1675,1569],{},[60,1677,1678],{"class":62,"line":158},[60,1679,1680],{},"COPY package*.json .\u002F\n",[60,1682,1683],{"class":62,"line":164},[60,1684,1685],{},"RUN npm ci --only=production\n",[60,1687,1688],{"class":62,"line":180},[60,1689,155],{"emptyLinePlaceholder":154},[60,1691,1692],{"class":62,"line":185},[60,1693,1694],{},"# Build stage  \n",[60,1696,1697],{"class":62,"line":191},[60,1698,1699],{},"FROM node:20-alpine AS builder\n",[60,1701,1702],{"class":62,"line":207},[60,1703,1569],{},[60,1705,1706],{"class":62,"line":212},[60,1707,1680],{},[60,1709,1710],{"class":62,"line":218},[60,1711,1712],{},"RUN npm ci\n",[60,1714,1715],{"class":62,"line":802},[60,1716,1602],{},[60,1718,1719],{"class":62,"line":807},[60,1720,1721],{},"RUN npm run build\n",[60,1723,1724],{"class":62,"line":813},[60,1725,155],{"emptyLinePlaceholder":154},[60,1727,1728],{"class":62,"line":821},[60,1729,1616],{},[60,1731,1732],{"class":62,"line":833},[60,1733,1734],{},"FROM node:20-alpine AS production\n",[60,1736,1737],{"class":62,"line":843},[60,1738,1569],{},[60,1740,1741],{"class":62,"line":851},[60,1742,1743],{},"ENV NODE_ENV=production\n",[60,1745,1746],{"class":62,"line":857},[60,1747,155],{"emptyLinePlaceholder":154},[60,1749,1750],{"class":62,"line":869},[60,1751,1752],{},"# Create non-root user\n",[60,1754,1755],{"class":62,"line":875},[60,1756,1757],{},"RUN addgroup -S appgroup && adduser -S appuser -G appgroup\n",[60,1759,1760],{"class":62,"line":881},[60,1761,155],{"emptyLinePlaceholder":154},[60,1763,1764],{"class":62,"line":886},[60,1765,1766],{},"# Copy only production dependencies\n",[60,1768,1769],{"class":62,"line":892},[60,1770,1771],{},"COPY --from=deps \u002Fapp\u002Fnode_modules .\u002Fnode_modules\n",[60,1773,1774],{"class":62,"line":898},[60,1775,1776],{},"# Copy built application\n",[60,1778,1779],{"class":62,"line":910},[60,1780,1781],{},"COPY --from=builder \u002Fapp\u002Fdist .\u002Fdist\n",[60,1783,1784],{"class":62,"line":916},[60,1785,1786],{},"COPY --from=builder \u002Fapp\u002Fpackage.json .\u002F\n",[60,1788,1789],{"class":62,"line":922},[60,1790,155],{"emptyLinePlaceholder":154},[60,1792,1793],{"class":62,"line":928},[60,1794,1795],{},"USER appuser\n",[60,1797,1798],{"class":62,"line":978},[60,1799,1800],{},"EXPOSE 3000\n",[60,1802,1803],{"class":62,"line":984},[60,1804,1805],{},"CMD [\"node\", \"dist\u002Findex.js\"]\n",[15,1807,1808,955],{},[19,1809,1810],{"index":1646},"Node.js application: Single stage (node:20): ~1.2GB, Multi-stage (node -> node:alpine): ~150MB (87% reduction)",[32,1812,1814],{"id":1813},"advanced-caching-strategies","Advanced Caching Strategies",[15,1816,1817],{},[19,1818,1820],{"index":1819},"7-31,7-32,7-33","While multi-stage builds reduce size, how you structure your instructions determines build speed. Docker caches layers based on the instructions used to create them. If an instruction changes (or the files it copies change), Docker invalidates that layer and every layer following it.",[15,1822,1823,1824],{},"The critical insight: ",[19,1825,1827],{"index":1826},"7-34,7-39,7-40","A common mistake is copying all source code before installing dependencies. It consequently invalidates the cache for RUN npm install, forcing a full re-installation of your dependencies. This adds minutes to your CI\u002FCD pipeline unnecessarily.",[15,1829,1830],{},[1169,1831,1832],{},"Anti-pattern:",[51,1834,1836],{"className":1550,"code":1835,"language":1552,"meta":56,"style":56},"COPY . .  # Changes every commit\nRUN npm install  # Cache invalidated every time\n",[46,1837,1838,1843],{"__ignoreMap":56},[60,1839,1840],{"class":62,"line":63},[60,1841,1842],{},"COPY . .  # Changes every commit\n",[60,1844,1845],{"class":62,"line":131},[60,1846,1847],{},"RUN npm install  # Cache invalidated every time\n",[15,1849,1850],{},[1169,1851,1852],{},"Optimized pattern:",[51,1854,1856],{"className":1550,"code":1855,"language":1552,"meta":56,"style":56},"COPY package*.json .\u002F  # Changes only when dependencies change\nRUN npm ci  # Cached until dependencies change\nCOPY . .  # Source changes don't affect dependency cache\nRUN npm run build\n",[46,1857,1858,1863,1868,1873],{"__ignoreMap":56},[60,1859,1860],{"class":62,"line":63},[60,1861,1862],{},"COPY package*.json .\u002F  # Changes only when dependencies change\n",[60,1864,1865],{"class":62,"line":131},[60,1866,1867],{},"RUN npm ci  # Cached until dependencies change\n",[60,1869,1870],{"class":62,"line":151},[60,1871,1872],{},"COPY . .  # Source changes don't affect dependency cache\n",[60,1874,1875],{"class":62,"line":158},[60,1876,1721],{},[32,1878,1880],{"id":1879},"buildkit-optimizations","BuildKit Optimizations",[15,1882,1883,1887],{},[19,1884,1886],{"index":1885},"1-20,1-22","BuildKit only builds the stages that the target stage depends on. There is no dependency on stage1, so it's skipped."," This means unused stages don't consume build time—critical for complex Dockerfiles with multiple build variants.",[15,1889,1890],{},[19,1891,1893],{"index":1892},"1-23","$ DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .",[15,1895,1896,1897,1900],{},"Use ",[46,1898,1899],{},"--target"," to build specific stages during development, dramatically reducing iteration time.",[32,1902,1904],{"id":1903},"security-implications","Security Implications",[15,1906,1907],{},[19,1908,1910],{"index":1909},"6-7,6-18","Smaller images mean faster pulls, less storage cost, reduced attack surface, and quicker container startup. They separate build-time concerns from runtime concerns, producing images that are smaller, faster to deploy, and more secure.",[15,1912,1913],{},"Every package, binary, and library in your image represents potential attack surface. Multi-stage builds let you include only what's absolutely necessary for runtime, eliminating entire categories of vulnerabilities.",[32,1915,602],{"id":601},[15,1917,1918],{},[19,1919,1921],{"index":1920},"10-16,10-17,10-18","You should also consider using two types of base image: one for building and unit testing, and another (typically slimmer) image for production. In the later stages of development, your image may not require build tools such as compilers, build systems, and debugging tools. A small image with minimal dependencies can considerably lower the attack surface.",[15,1923,1924,1925,1928],{},"For ultimate optimization, use ",[46,1926,1927],{},"distroless"," images for your production stage. Google's distroless images contain only your application and runtime dependencies—no shell, no package manager, no unnecessary binaries:",[51,1930,1932],{"className":1550,"code":1931,"language":1552,"meta":56,"style":56},"FROM gcr.io\u002Fdistroless\u002Fnodejs20-debian12\nCOPY --from=builder \u002Fapp\u002Fdist .\u002F\nEXPOSE 3000\nCMD [\"index.js\"]\n",[46,1933,1934,1939,1944,1948],{"__ignoreMap":56},[60,1935,1936],{"class":62,"line":63},[60,1937,1938],{},"FROM gcr.io\u002Fdistroless\u002Fnodejs20-debian12\n",[60,1940,1941],{"class":62,"line":131},[60,1942,1943],{},"COPY --from=builder \u002Fapp\u002Fdist .\u002F\n",[60,1945,1946],{"class":62,"line":151},[60,1947,1800],{},[60,1949,1950],{"class":62,"line":158},[60,1951,1952],{},"CMD [\"index.js\"]\n",[32,1954,1394],{"id":1393},[15,1956,1957],{},"Here's a production-ready multi-stage build for a TypeScript API:",[51,1959,1961],{"className":1550,"code":1960,"language":1552,"meta":56,"style":56},"# syntax=docker\u002Fdockerfile:1\n\n# Dependencies stage\nFROM node:20-alpine AS deps\nWORKDIR \u002Fapp\nCOPY package*.json .\u002F\nRUN npm ci --only=production && npm cache clean --force\n\n# Build stage\nFROM node:20-alpine AS builder\nWORKDIR \u002Fapp\nCOPY package*.json .\u002F\nRUN npm ci\nCOPY . .\nRUN npm run build && npm run test\n\n# Production stage\nFROM gcr.io\u002Fdistroless\u002Fnodejs20-debian12 AS production\nWORKDIR \u002Fapp\nCOPY --from=deps \u002Fapp\u002Fnode_modules .\u002Fnode_modules\nCOPY --from=builder \u002Fapp\u002Fdist .\u002Fdist\nCOPY --from=builder \u002Fapp\u002Fpackage.json .\u002F\nEXPOSE 3000\nCMD [\"dist\u002Findex.js\"]\n",[46,1962,1963,1968,1972,1976,1980,1984,1988,1993,1997,2001,2005,2009,2013,2017,2021,2026,2030,2034,2039,2043,2047,2051,2055,2059],{"__ignoreMap":56},[60,1964,1965],{"class":62,"line":63},[60,1966,1967],{},"# syntax=docker\u002Fdockerfile:1\n",[60,1969,1970],{"class":62,"line":131},[60,1971,155],{"emptyLinePlaceholder":154},[60,1973,1974],{"class":62,"line":151},[60,1975,1666],{},[60,1977,1978],{"class":62,"line":158},[60,1979,1671],{},[60,1981,1982],{"class":62,"line":164},[60,1983,1569],{},[60,1985,1986],{"class":62,"line":180},[60,1987,1680],{},[60,1989,1990],{"class":62,"line":185},[60,1991,1992],{},"RUN npm ci --only=production && npm cache clean --force\n",[60,1994,1995],{"class":62,"line":191},[60,1996,155],{"emptyLinePlaceholder":154},[60,1998,1999],{"class":62,"line":207},[60,2000,1559],{},[60,2002,2003],{"class":62,"line":212},[60,2004,1699],{},[60,2006,2007],{"class":62,"line":218},[60,2008,1569],{},[60,2010,2011],{"class":62,"line":802},[60,2012,1680],{},[60,2014,2015],{"class":62,"line":807},[60,2016,1712],{},[60,2018,2019],{"class":62,"line":813},[60,2020,1602],{},[60,2022,2023],{"class":62,"line":821},[60,2024,2025],{},"RUN npm run build && npm run test\n",[60,2027,2028],{"class":62,"line":833},[60,2029,155],{"emptyLinePlaceholder":154},[60,2031,2032],{"class":62,"line":843},[60,2033,1616],{},[60,2035,2036],{"class":62,"line":851},[60,2037,2038],{},"FROM gcr.io\u002Fdistroless\u002Fnodejs20-debian12 AS production\n",[60,2040,2041],{"class":62,"line":857},[60,2042,1569],{},[60,2044,2045],{"class":62,"line":869},[60,2046,1771],{},[60,2048,2049],{"class":62,"line":875},[60,2050,1781],{},[60,2052,2053],{"class":62,"line":881},[60,2054,1786],{},[60,2056,2057],{"class":62,"line":886},[60,2058,1800],{},[60,2060,2061],{"class":62,"line":892},[60,2062,2063],{},"CMD [\"dist\u002Findex.js\"]\n",[15,2065,2066],{},"This pattern ensures your production image contains zero build tools, zero source code, and zero development dependencies—just the compiled application and its runtime requirements.",[1061,2068,1457],{},{"title":56,"searchDepth":131,"depth":131,"links":2070},[2071,2072,2073,2077,2078,2079,2080,2081],{"id":1505,"depth":131,"text":1506},{"id":1525,"depth":131,"text":1526},{"id":1541,"depth":131,"text":1542,"children":2074},[2075,2076],{"id":1546,"depth":151,"text":1547},{"id":1655,"depth":151,"text":1656},{"id":1813,"depth":131,"text":1814},{"id":1879,"depth":131,"text":1880},{"id":1903,"depth":131,"text":1904},{"id":601,"depth":131,"text":602},{"id":1393,"depth":131,"text":1394},"2026-04-01","A typical Node.js Docker image built the naive way can be over 1 GB. The same application built with multi-stage builds can be under 100 MB. This isn't just about saving disk space—it's about fundamentally rethinking how we build production containers.",{},"\u002Flessons\u002F2026-04-01",{"title":1484,"description":2083},"lessons\u002F2026-04-01","docker-multi-stage",[2090,2091,2092,2093],"docker","containers","optimization","production","Master Docker multi-stage builds to create production containers that are 90% smaller, more secure, and faster to deploy by separating build environments from runtime environments.","devops",false,"l4S8VpFgBYq2vbYfZTJrCGiecycrizYYfFzlscrSVac",{"id":2099,"title":2100,"body":2101,"date":2349,"description":2108,"difficulty":1075,"extension":1076,"format":2350,"meta":2351,"navigation":154,"path":2352,"progression":63,"seo":2353,"stem":2354,"subtopic":2355,"tags":2356,"tldr":2361,"topic":2095,"triggered":154,"__hash__":2362},"lessons\u002Flessons\u002F2026-03-31.md","Claude's Artifacts: Interactive Code Generation",{"type":8,"value":2102,"toc":2340},[2103,2106,2109,2112,2116,2119,2151,2154,2158,2161,2164,2167,2171,2174,2177,2181,2184,2190,2196,2202,2208,2212,2215,2218,2220,2223,2225,2335,2338],[11,2104,2100],{"id":2105},"claudes-artifacts-interactive-code-generation",[15,2107,2108],{},"Claude's Artifacts feature transforms how developers interact with AI-generated code. Instead of receiving static code blocks that require copy-pasting into separate environments, Artifacts creates live, interactive previews directly within the conversation interface.",[15,2110,2111],{},"When Claude generates substantial code—whether it's a React component, HTML page, SVG diagram, or data visualization—it automatically creates an Artifact. This isn't just syntax-highlighted text; it's a fully functional preview that renders in real-time alongside the conversation.",[32,2113,2115],{"id":2114},"the-technical-architecture","The Technical Architecture",[15,2117,2118],{},"Artifacts operate within a sandboxed iframe environment, providing secure execution of user code without compromising the host application. The system supports multiple content types:",[1210,2120,2121,2127,2133,2139,2145],{},[1166,2122,2123,2126],{},[1169,2124,2125],{},"React Components",": Full JSX with hooks, state management, and modern React patterns",[1166,2128,2129,2132],{},[1169,2130,2131],{},"HTML\u002FCSS\u002FJS",": Complete web pages with interactive functionality",[1166,2134,2135,2138],{},[1169,2136,2137],{},"SVG Graphics",": Vector illustrations and diagrams with programmatic generation",[1166,2140,2141,2144],{},[1169,2142,2143],{},"Data Visualizations",": Charts and graphs using libraries like D3.js",[1166,2146,2147,2150],{},[1169,2148,2149],{},"Markdown Documents",": Rich text with formatting and structure",[15,2152,2153],{},"The sandbox includes a curated set of popular libraries—React, D3, Chart.js, Tailwind CSS—eliminating the friction of dependency management for rapid prototyping.",[32,2155,2157],{"id":2156},"iterative-development-workflow","Iterative Development Workflow",[15,2159,2160],{},"The real power emerges in the iterative development cycle. Rather than describing changes and hoping the AI understands your intent, you can reference specific elements in the live preview:",[15,2162,2163],{},"\"Make the header blue\" becomes unambiguous when both you and Claude are looking at the same rendered output. The AI can see exactly which header you mean, understand the current styling context, and make precise modifications.",[15,2165,2166],{},"This creates a collaborative debugging environment where you can spot issues immediately, point them out conversationally, and see fixes applied in real-time. The traditional cycle of generate → copy → paste → run → debug compresses into a single, fluid interaction.",[32,2168,2170],{"id":2169},"beyond-simple-code-generation","Beyond Simple Code Generation",[15,2172,2173],{},"Artifacts excel at exploratory programming—those moments when you're not sure exactly what you want but know you'll recognize it when you see it. Starting with a rough concept like \"a dashboard for monitoring server metrics\" can evolve through dozens of iterations, with each change building naturally on the previous version.",[15,2175,2176],{},"The persistence model maintains conversation context across modifications. Claude remembers the evolution of your code, understanding not just the current state but the design decisions and constraints that led there. This historical awareness enables more intelligent suggestions and prevents regression of intentional design choices.",[32,2178,2180],{"id":2179},"integration-patterns","Integration Patterns",[15,2182,2183],{},"Artifacts work particularly well for:",[15,2185,2186,2189],{},[1169,2187,2188],{},"Rapid UI Prototyping",": Sketch out component hierarchies and user flows without setting up build tools or project scaffolding.",[15,2191,2192,2195],{},[1169,2193,2194],{},"Data Exploration",": Upload CSV files or describe datasets, then iterate through different visualization approaches to discover insights.",[15,2197,2198,2201],{},[1169,2199,2200],{},"Algorithm Visualization",": Implement and visualize sorting algorithms, graph traversals, or mathematical functions with immediate visual feedback.",[15,2203,2204,2207],{},[1169,2205,2206],{},"Documentation Creation",": Generate interactive examples within technical documentation, allowing readers to experiment with parameters and see results.",[32,2209,2211],{"id":2210},"limitations-and-considerations","Limitations and Considerations",[15,2213,2214],{},"The sandbox environment has intentional constraints. No network requests, no file system access, no server-side functionality. These limitations maintain security but mean Artifacts work best for frontend code, data processing, and visualization rather than full-stack applications.",[15,2216,2217],{},"The ephemeral nature of Artifacts means they exist only within the conversation context. While you can export the code, there's no built-in version control or project management. For production work, Artifacts serve as a starting point rather than a complete development environment.",[32,2219,602],{"id":601},[15,2221,2222],{},"When working with complex Artifacts, use descriptive variable names and add inline comments explaining your intent. Claude can see your code structure and will maintain consistency with your naming conventions and architectural patterns across iterations. This makes the collaboration feel more like pair programming than prompt engineering.",[32,2224,1394],{"id":1393},[51,2226,2230],{"className":2227,"code":2228,"language":2229,"meta":56,"style":56},"language-javascript shiki shiki-themes github-light","\u002F\u002F Request: \"Create an interactive expense tracker with categories\"\n\u002F\u002F Claude generates a React component with:\n\nconst ExpenseTracker = () => {\n  const [expenses, setExpenses] = useState([]);\n  const [category, setCategory] = useState('');\n  const [amount, setAmount] = useState('');\n  \n  \u002F\u002F Live preview shows functional form with validation\n  \u002F\u002F You can immediately test adding expenses, see the category breakdown\n  \u002F\u002F Then refine: \"Add a monthly spending limit with visual indicators\"\n  \n  return (\n    \u003Cdiv className=\"p-6 max-w-md mx-auto\">\n      \u003Ch2 className=\"text-2xl mb-4\">Expense Tracker\u003C\u002Fh2>\n      {\u002F* Interactive form renders immediately *\u002F}\n      \u003CExpenseForm onAdd={addExpense} \u002F>\n      \u003CCategorySummary expenses={expenses} \u002F>\n    \u003C\u002Fdiv>\n  );\n};\n","javascript",[46,2231,2232,2237,2242,2246,2251,2256,2261,2266,2271,2276,2281,2286,2290,2295,2300,2305,2310,2315,2320,2325,2330],{"__ignoreMap":56},[60,2233,2234],{"class":62,"line":63},[60,2235,2236],{},"\u002F\u002F Request: \"Create an interactive expense tracker with categories\"\n",[60,2238,2239],{"class":62,"line":131},[60,2240,2241],{},"\u002F\u002F Claude generates a React component with:\n",[60,2243,2244],{"class":62,"line":151},[60,2245,155],{"emptyLinePlaceholder":154},[60,2247,2248],{"class":62,"line":158},[60,2249,2250],{},"const ExpenseTracker = () => {\n",[60,2252,2253],{"class":62,"line":164},[60,2254,2255],{},"  const [expenses, setExpenses] = useState([]);\n",[60,2257,2258],{"class":62,"line":180},[60,2259,2260],{},"  const [category, setCategory] = useState('');\n",[60,2262,2263],{"class":62,"line":185},[60,2264,2265],{},"  const [amount, setAmount] = useState('');\n",[60,2267,2268],{"class":62,"line":191},[60,2269,2270],{},"  \n",[60,2272,2273],{"class":62,"line":207},[60,2274,2275],{},"  \u002F\u002F Live preview shows functional form with validation\n",[60,2277,2278],{"class":62,"line":212},[60,2279,2280],{},"  \u002F\u002F You can immediately test adding expenses, see the category breakdown\n",[60,2282,2283],{"class":62,"line":218},[60,2284,2285],{},"  \u002F\u002F Then refine: \"Add a monthly spending limit with visual indicators\"\n",[60,2287,2288],{"class":62,"line":802},[60,2289,2270],{},[60,2291,2292],{"class":62,"line":807},[60,2293,2294],{},"  return (\n",[60,2296,2297],{"class":62,"line":813},[60,2298,2299],{},"    \u003Cdiv className=\"p-6 max-w-md mx-auto\">\n",[60,2301,2302],{"class":62,"line":821},[60,2303,2304],{},"      \u003Ch2 className=\"text-2xl mb-4\">Expense Tracker\u003C\u002Fh2>\n",[60,2306,2307],{"class":62,"line":833},[60,2308,2309],{},"      {\u002F* Interactive form renders immediately *\u002F}\n",[60,2311,2312],{"class":62,"line":843},[60,2313,2314],{},"      \u003CExpenseForm onAdd={addExpense} \u002F>\n",[60,2316,2317],{"class":62,"line":851},[60,2318,2319],{},"      \u003CCategorySummary expenses={expenses} \u002F>\n",[60,2321,2322],{"class":62,"line":857},[60,2323,2324],{},"    \u003C\u002Fdiv>\n",[60,2326,2327],{"class":62,"line":869},[60,2328,2329],{},"  );\n",[60,2331,2332],{"class":62,"line":875},[60,2333,2334],{},"};\n",[15,2336,2337],{},"The conversation continues with visual refinements, feature additions, and bug fixes—all happening in the live preview without breaking context.",[1061,2339,1457],{},{"title":56,"searchDepth":131,"depth":131,"links":2341},[2342,2343,2344,2345,2346,2347,2348],{"id":2114,"depth":131,"text":2115},{"id":2156,"depth":131,"text":2157},{"id":2169,"depth":131,"text":2170},{"id":2179,"depth":131,"text":2180},{"id":2210,"depth":131,"text":2211},{"id":601,"depth":131,"text":602},{"id":1393,"depth":131,"text":1394},"2026-03-31","explainer",{},"\u002Flessons\u002F2026-03-31",{"title":2100,"description":2108},"lessons\u002F2026-03-31","claude-artifacts",[2357,2358,2359,2360],"claude","ai-tools","prototyping","interactive-development","Claude's Artifacts feature creates interactive, editable code environments within conversations, enabling real-time prototyping and iterative development without leaving the chat interface.","RuJdRhKloPAoXEcBXsyr_B3MQpCgfCZcYW-nhQTqOHM",{"id":2364,"title":2365,"body":2366,"date":3061,"description":3062,"difficulty":1075,"extension":1076,"format":3063,"meta":3064,"navigation":154,"path":3065,"progression":131,"seo":3066,"stem":3067,"subtopic":3068,"tags":3069,"tldr":3072,"topic":3073,"triggered":2096,"__hash__":3074},"lessons\u002Flessons\u002F2026-03-27.md","Shell Parameter Expansion: Beyond ${var}",{"type":8,"value":2367,"toc":3051},[2368,2375,2379,2436,2443,2447,2502,2506,2617,2621,2693,2697,2744,2748,2777,2779,2782,2822,2825,2892,2894,2897,3034,3048],[15,2369,2370,2371,2374],{},"Every time you write ",[46,2372,2373],{},"$(echo \"$var\" | sed 's\u002Ffoo\u002Fbar\u002F')"," in a shell script, there's probably a parameter expansion that does it faster and without spawning a subprocess.",[32,2376,2378],{"id":2377},"default-values","Default Values",[51,2380,2382],{"className":53,"code":2381,"language":55,"meta":56,"style":56},"${var:-default}    # Use default if var is unset or empty\n${var:=default}    # Assign default if var is unset or empty\n${var:+alternate}  # Use alternate if var IS set and non-empty\n${var:?error msg}  # Exit with error if var is unset or empty\n",[46,2383,2384,2398,2410,2423],{"__ignoreMap":56},[60,2385,2386,2389,2392,2395],{"class":62,"line":63},[60,2387,2388],{"class":70},"${var",[60,2390,2391],{"class":66},":-",[60,2393,2394],{"class":70},"default}    ",[60,2396,2397],{"class":127},"# Use default if var is unset or empty\n",[60,2399,2400,2402,2405,2407],{"class":62,"line":131},[60,2401,2388],{"class":70},[60,2403,2404],{"class":66},":=",[60,2406,2394],{"class":70},[60,2408,2409],{"class":127},"# Assign default if var is unset or empty\n",[60,2411,2412,2414,2417,2420],{"class":62,"line":151},[60,2413,2388],{"class":70},[60,2415,2416],{"class":66},":",[60,2418,2419],{"class":70},"+alternate}  ",[60,2421,2422],{"class":127},"# Use alternate if var IS set and non-empty\n",[60,2424,2425,2427,2430,2433],{"class":62,"line":158},[60,2426,2388],{"class":70},[60,2428,2429],{"class":66},":?",[60,2431,2432],{"class":70},"error msg}  ",[60,2434,2435],{"class":127},"# Exit with error if var is unset or empty\n",[15,2437,2438,2439,2442],{},"The colon matters. Without it (",[46,2440,2441],{},"${var-default}","), only checks if unset, not empty.",[32,2444,2446],{"id":2445},"string-manipulation","String Manipulation",[51,2448,2450],{"className":53,"code":2449,"language":55,"meta":56,"style":56},"${var#pattern}     # Remove shortest match from beginning\n${var##pattern}    # Remove longest match from beginning\n${var%pattern}     # Remove shortest match from end\n${var%%pattern}    # Remove longest match from end\n",[46,2451,2452,2465,2478,2490],{"__ignoreMap":56},[60,2453,2454,2456,2459,2462],{"class":62,"line":63},[60,2455,2388],{"class":70},[60,2457,2458],{"class":66},"#",[60,2460,2461],{"class":70},"pattern}     ",[60,2463,2464],{"class":127},"# Remove shortest match from beginning\n",[60,2466,2467,2469,2472,2475],{"class":62,"line":131},[60,2468,2388],{"class":70},[60,2470,2471],{"class":66},"##",[60,2473,2474],{"class":70},"pattern}    ",[60,2476,2477],{"class":127},"# Remove longest match from beginning\n",[60,2479,2480,2482,2485,2487],{"class":62,"line":151},[60,2481,2388],{"class":70},[60,2483,2484],{"class":66},"%",[60,2486,2461],{"class":70},[60,2488,2489],{"class":127},"# Remove shortest match from end\n",[60,2491,2492,2494,2497,2499],{"class":62,"line":158},[60,2493,2388],{"class":70},[60,2495,2496],{"class":66},"%%",[60,2498,2474],{"class":70},[60,2500,2501],{"class":127},"# Remove longest match from end\n",[32,2503,2505],{"id":2504},"real-examples","Real Examples",[51,2507,2509],{"className":53,"code":2508,"language":55,"meta":56,"style":56},"filepath=\"\u002Fhome\u002Fuser\u002Fdocuments\u002Freport.tar.gz\"\n\n${filepath##*\u002F}      # report.tar.gz  (basename)\n${filepath%\u002F*}       # \u002Fhome\u002Fuser\u002Fdocuments  (dirname)\n\nfilename=\"${filepath##*\u002F}\"\n${filename%%.*}      # report  (name without any extension)\n${filename%.*}       # report.tar  (name without last extension)\n${filename##*.}      # gz  (last extension only)\n",[46,2510,2511,2521,2525,2539,2552,2556,2573,2589,2604],{"__ignoreMap":56},[60,2512,2513,2516,2518],{"class":62,"line":63},[60,2514,2515],{"class":70},"filepath",[60,2517,74],{"class":66},[60,2519,2520],{"class":77},"\"\u002Fhome\u002Fuser\u002Fdocuments\u002Freport.tar.gz\"\n",[60,2522,2523],{"class":62,"line":131},[60,2524,155],{"emptyLinePlaceholder":154},[60,2526,2527,2530,2533,2536],{"class":62,"line":151},[60,2528,2529],{"class":70},"${filepath",[60,2531,2532],{"class":66},"##*\u002F",[60,2534,2535],{"class":70},"}      ",[60,2537,2538],{"class":127},"# report.tar.gz  (basename)\n",[60,2540,2541,2543,2546,2549],{"class":62,"line":158},[60,2542,2529],{"class":70},[60,2544,2545],{"class":66},"%\u002F*",[60,2547,2548],{"class":70},"}       ",[60,2550,2551],{"class":127},"# \u002Fhome\u002Fuser\u002Fdocuments  (dirname)\n",[60,2553,2554],{"class":62,"line":164},[60,2555,155],{"emptyLinePlaceholder":154},[60,2557,2558,2561,2563,2566,2568,2570],{"class":62,"line":180},[60,2559,2560],{"class":70},"filename",[60,2562,74],{"class":66},[60,2564,2565],{"class":77},"\"${",[60,2567,2515],{"class":70},[60,2569,2532],{"class":66},[60,2571,2572],{"class":77},"}\"\n",[60,2574,2575,2578,2580,2582,2584,2586],{"class":62,"line":185},[60,2576,2577],{"class":70},"${filename",[60,2579,2496],{"class":66},[60,2581,955],{"class":70},[60,2583,904],{"class":66},[60,2585,2535],{"class":70},[60,2587,2588],{"class":127},"# report  (name without any extension)\n",[60,2590,2591,2593,2595,2597,2599,2601],{"class":62,"line":191},[60,2592,2577],{"class":70},[60,2594,2484],{"class":66},[60,2596,955],{"class":70},[60,2598,904],{"class":66},[60,2600,2548],{"class":70},[60,2602,2603],{"class":127},"# report.tar  (name without last extension)\n",[60,2605,2606,2608,2611,2614],{"class":62,"line":207},[60,2607,2577],{"class":70},[60,2609,2610],{"class":66},"##*",[60,2612,2613],{"class":70},".}      ",[60,2615,2616],{"class":127},"# gz  (last extension only)\n",[32,2618,2620],{"id":2619},"substitution","Substitution",[51,2622,2624],{"className":53,"code":2623,"language":55,"meta":56,"style":56},"${var\u002Fpattern\u002Freplacement}   # Replace first match\n${var\u002F\u002Fpattern\u002Freplacement}  # Replace all matches\n${var\u002F#pattern\u002Freplacement}  # Replace if at beginning\n${var\u002F%pattern\u002Freplacement}  # Replace if at end\n",[46,2625,2626,2644,2661,2677],{"__ignoreMap":56},[60,2627,2628,2630,2633,2636,2638,2641],{"class":62,"line":63},[60,2629,2388],{"class":70},[60,2631,2632],{"class":66},"\u002F",[60,2634,2635],{"class":70},"pattern",[60,2637,2632],{"class":66},[60,2639,2640],{"class":70},"replacement}   ",[60,2642,2643],{"class":127},"# Replace first match\n",[60,2645,2646,2648,2651,2653,2655,2658],{"class":62,"line":131},[60,2647,2388],{"class":70},[60,2649,2650],{"class":66},"\u002F\u002F",[60,2652,2635],{"class":70},[60,2654,2632],{"class":66},[60,2656,2657],{"class":70},"replacement}  ",[60,2659,2660],{"class":127},"# Replace all matches\n",[60,2662,2663,2665,2668,2670,2672,2674],{"class":62,"line":151},[60,2664,2388],{"class":70},[60,2666,2667],{"class":66},"\u002F#",[60,2669,2635],{"class":70},[60,2671,2632],{"class":66},[60,2673,2657],{"class":70},[60,2675,2676],{"class":127},"# Replace if at beginning\n",[60,2678,2679,2681,2684,2686,2688,2690],{"class":62,"line":158},[60,2680,2388],{"class":70},[60,2682,2683],{"class":66},"\u002F%",[60,2685,2635],{"class":70},[60,2687,2632],{"class":66},[60,2689,2657],{"class":70},[60,2691,2692],{"class":127},"# Replace if at end\n",[32,2694,2696],{"id":2695},"length-and-slicing","Length and Slicing",[51,2698,2700],{"className":53,"code":2699,"language":55,"meta":56,"style":56},"${#var}              # String length\n${var:offset}        # Substring from offset\n${var:offset:length} # Substring with length\n",[46,2701,2702,2715,2727],{"__ignoreMap":56},[60,2703,2704,2707,2709,2712],{"class":62,"line":63},[60,2705,2706],{"class":70},"${",[60,2708,2458],{"class":66},[60,2710,2711],{"class":70},"var}              ",[60,2713,2714],{"class":127},"# String length\n",[60,2716,2717,2719,2721,2724],{"class":62,"line":131},[60,2718,2388],{"class":70},[60,2720,2416],{"class":66},[60,2722,2723],{"class":70},"offset}        ",[60,2725,2726],{"class":127},"# Substring from offset\n",[60,2728,2729,2731,2733,2736,2738,2741],{"class":62,"line":151},[60,2730,2388],{"class":70},[60,2732,2416],{"class":66},[60,2734,2735],{"class":70},"offset",[60,2737,2416],{"class":66},[60,2739,2740],{"class":70},"length} ",[60,2742,2743],{"class":127},"# Substring with length\n",[32,2745,2747],{"id":2746},"case-conversion-bash-4","Case Conversion (Bash 4+)",[51,2749,2751],{"className":53,"code":2750,"language":55,"meta":56,"style":56},"${var^^}    # UPPERCASE\n${var,,}    # lowercase\n${var^}     # Capitalize first letter\n",[46,2752,2753,2761,2769],{"__ignoreMap":56},[60,2754,2755,2758],{"class":62,"line":63},[60,2756,2757],{"class":70},"${var^^}    ",[60,2759,2760],{"class":127},"# UPPERCASE\n",[60,2762,2763,2766],{"class":62,"line":131},[60,2764,2765],{"class":70},"${var,,}    ",[60,2767,2768],{"class":127},"# lowercase\n",[60,2770,2771,2774],{"class":62,"line":151},[60,2772,2773],{"class":70},"${var^}     ",[60,2775,2776],{"class":127},"# Capitalize first letter\n",[32,2778,602],{"id":601},[15,2780,2781],{},"Combine expansions for powerful one-liners. Convert a filename from snake_case to kebab-case without any external tools:",[51,2783,2785],{"className":53,"code":2784,"language":55,"meta":56,"style":56},"file=\"my_cool_script.sh\"\necho \"${file\u002F\u002F_\u002F-}\"   # my-cool-script.sh\n",[46,2786,2787,2797],{"__ignoreMap":56},[60,2788,2789,2792,2794],{"class":62,"line":63},[60,2790,2791],{"class":70},"file",[60,2793,74],{"class":66},[60,2795,2796],{"class":77},"\"my_cool_script.sh\"\n",[60,2798,2799,2801,2804,2806,2808,2811,2813,2816,2819],{"class":62,"line":131},[60,2800,740],{"class":138},[60,2802,2803],{"class":77}," \"${",[60,2805,2791],{"class":70},[60,2807,2650],{"class":66},[60,2809,2810],{"class":70},"_",[60,2812,2632],{"class":66},[60,2814,2815],{"class":70},"-",[60,2817,2818],{"class":77},"}\"",[60,2820,2821],{"class":127},"   # my-cool-script.sh\n",[15,2823,2824],{},"Or strip a common prefix from all files in a directory:",[51,2826,2828],{"className":53,"code":2827,"language":55,"meta":56,"style":56},"prefix=\"project_v2_\"\nfor f in ${prefix}*; do\n  mv \"$f\" \"${f#$prefix}\"\ndone\n",[46,2829,2830,2840,2862,2887],{"__ignoreMap":56},[60,2831,2832,2835,2837],{"class":62,"line":63},[60,2833,2834],{"class":70},"prefix",[60,2836,74],{"class":66},[60,2838,2839],{"class":77},"\"project_v2_\"\n",[60,2841,2842,2845,2848,2851,2854,2856,2859],{"class":62,"line":131},[60,2843,2844],{"class":66},"for",[60,2846,2847],{"class":70}," f ",[60,2849,2850],{"class":66},"in",[60,2852,2853],{"class":70}," ${prefix}",[60,2855,904],{"class":77},[60,2857,2858],{"class":70},"; ",[60,2860,2861],{"class":66},"do\n",[60,2863,2864,2867,2870,2873,2875,2877,2880,2882,2885],{"class":62,"line":151},[60,2865,2866],{"class":134},"  mv",[60,2868,2869],{"class":77}," \"",[60,2871,2872],{"class":70},"$f",[60,2874,78],{"class":77},[60,2876,2803],{"class":77},[60,2878,2879],{"class":70},"f",[60,2881,2458],{"class":66},[60,2883,2884],{"class":70},"$prefix",[60,2886,2572],{"class":77},[60,2888,2889],{"class":62,"line":158},[60,2890,2891],{"class":66},"done\n",[32,2893,1394],{"id":1393},[15,2895,2896],{},"A practical script that processes log file paths — extracting date, service name, and rotating based on age — using nothing but parameter expansion:",[51,2898,2900],{"className":53,"code":2899,"language":55,"meta":56,"style":56},"#!\u002Fbin\u002Fbash\nfor logfile in \u002Fvar\u002Flog\u002Fservices\u002F*.2026-*.log; do\n  base=\"${logfile##*\u002F}\"             # api-gateway.2026-03-15.log\n  service=\"${base%%.*}\"            # api-gateway\n  date_part=\"${base#*.}\"           # 2026-03-15.log\n  date_part=\"${date_part%.*}\"      # 2026-03-15\n\n  echo \"Service: $service, Date: $date_part\"\ndone\n",[46,2901,2902,2906,2922,2941,2964,2984,3006,3010,3030],{"__ignoreMap":56},[60,2903,2904],{"class":62,"line":63},[60,2905,721],{"class":127},[60,2907,2908,2910,2913,2915,2918,2920],{"class":62,"line":131},[60,2909,2844],{"class":66},[60,2911,2912],{"class":70}," logfile ",[60,2914,2850],{"class":66},[60,2916,2917],{"class":77}," \u002Fvar\u002Flog\u002Fservices\u002F*.2026-*.log",[60,2919,2858],{"class":70},[60,2921,2861],{"class":66},[60,2923,2924,2927,2929,2931,2934,2936,2938],{"class":62,"line":151},[60,2925,2926],{"class":70},"  base",[60,2928,74],{"class":66},[60,2930,2565],{"class":77},[60,2932,2933],{"class":70},"logfile",[60,2935,2532],{"class":66},[60,2937,2818],{"class":77},[60,2939,2940],{"class":127},"             # api-gateway.2026-03-15.log\n",[60,2942,2943,2946,2948,2950,2953,2955,2957,2959,2961],{"class":62,"line":158},[60,2944,2945],{"class":70},"  service",[60,2947,74],{"class":66},[60,2949,2565],{"class":77},[60,2951,2952],{"class":70},"base",[60,2954,2496],{"class":66},[60,2956,955],{"class":77},[60,2958,904],{"class":66},[60,2960,2818],{"class":77},[60,2962,2963],{"class":127},"            # api-gateway\n",[60,2965,2966,2969,2971,2973,2975,2978,2981],{"class":62,"line":164},[60,2967,2968],{"class":70},"  date_part",[60,2970,74],{"class":66},[60,2972,2565],{"class":77},[60,2974,2952],{"class":70},[60,2976,2977],{"class":66},"#*",[60,2979,2980],{"class":77},".}\"",[60,2982,2983],{"class":127},"           # 2026-03-15.log\n",[60,2985,2986,2988,2990,2992,2995,2997,2999,3001,3003],{"class":62,"line":180},[60,2987,2968],{"class":70},[60,2989,74],{"class":66},[60,2991,2565],{"class":77},[60,2993,2994],{"class":70},"date_part",[60,2996,2484],{"class":66},[60,2998,955],{"class":77},[60,3000,904],{"class":66},[60,3002,2818],{"class":77},[60,3004,3005],{"class":127},"      # 2026-03-15\n",[60,3007,3008],{"class":62,"line":185},[60,3009,155],{"emptyLinePlaceholder":154},[60,3011,3012,3015,3018,3021,3024,3027],{"class":62,"line":191},[60,3013,3014],{"class":138},"  echo",[60,3016,3017],{"class":77}," \"Service: ",[60,3019,3020],{"class":70},"$service",[60,3022,3023],{"class":77},", Date: ",[60,3025,3026],{"class":70},"$date_part",[60,3028,3029],{"class":77},"\"\n",[60,3031,3032],{"class":62,"line":207},[60,3033,2891],{"class":66},[15,3035,3036,3037,3040,3041,3040,3044,3047],{},"No ",[46,3038,3039],{},"awk",". No ",[46,3042,3043],{},"sed",[46,3045,3046],{},"cut",". Just the shell doing what it was designed to do.",[1061,3049,3050],{},"html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}",{"title":56,"searchDepth":131,"depth":131,"links":3052},[3053,3054,3055,3056,3057,3058,3059,3060],{"id":2377,"depth":131,"text":2378},{"id":2445,"depth":131,"text":2446},{"id":2504,"depth":131,"text":2505},{"id":2619,"depth":131,"text":2620},{"id":2695,"depth":131,"text":2696},{"id":2746,"depth":131,"text":2747},{"id":601,"depth":131,"text":602},{"id":1393,"depth":131,"text":1394},"2026-03-27","Every time you write $(echo \"$var\" | sed 's\u002Ffoo\u002Fbar\u002F') in a shell script, there's probably a parameter expansion that does it faster and without spawning a subprocess.","cheatsheet",{},"\u002Flessons\u002F2026-03-27",{"title":2365,"description":3062},"lessons\u002F2026-03-27","parameter-expansion",[55,3070,3068,3071],"shell","scripting","Bash parameter expansion can replace most uses of sed, awk, and cut for string manipulation. Learn the syntax once, use it everywhere.","cli","oXaRIcLDNOFw16Ze5aSwfMatBLD24AyyISCuhjITzKI",{"id":3076,"title":3077,"body":3078,"date":3482,"description":3483,"difficulty":3484,"extension":1076,"format":3485,"meta":3486,"navigation":154,"path":3487,"progression":151,"seo":3488,"stem":3489,"subtopic":3490,"tags":3491,"tldr":3495,"topic":3104,"triggered":2096,"__hash__":3496},"lessons\u002Flessons\u002F2026-03-26.md","The Art of git rebase --onto",{"type":8,"value":3079,"toc":3474},[3080,3091,3095,3143,3157,3161,3181,3231,3239,3243,3246,3282,3285,3289,3292,3340,3346,3348,3359,3370,3372,3375,3460,3471],[15,3081,3082,3083,3086,3087,3090],{},"Standard ",[46,3084,3085],{},"git rebase"," replays your branch commits on top of another branch. But ",[46,3088,3089],{},"git rebase --onto"," gives you a third argument — and with it, surgical control over exactly which commits go where.",[32,3092,3094],{"id":3093},"the-three-argument-form","The Three-Argument Form",[51,3096,3098],{"className":53,"code":3097,"language":55,"meta":56,"style":56},"git rebase --onto \u003Cnewbase> \u003Coldbase> \u003Cbranch>\n",[46,3099,3100],{"__ignoreMap":56},[60,3101,3102,3105,3108,3111,3114,3117,3120,3123,3125,3128,3130,3132,3134,3137,3140],{"class":62,"line":63},[60,3103,3104],{"class":134},"git",[60,3106,3107],{"class":77}," rebase",[60,3109,3110],{"class":138}," --onto",[60,3112,3113],{"class":66}," \u003C",[60,3115,3116],{"class":77},"newbas",[60,3118,3119],{"class":70},"e",[60,3121,3122],{"class":66},">",[60,3124,3113],{"class":66},[60,3126,3127],{"class":77},"oldbas",[60,3129,3119],{"class":70},[60,3131,3122],{"class":66},[60,3133,3113],{"class":66},[60,3135,3136],{"class":77},"branc",[60,3138,3139],{"class":70},"h",[60,3141,3142],{"class":66},">\n",[15,3144,3145,3146,3149,3150,3153,3154,955],{},"Translation: take the commits between ",[46,3147,3148],{},"oldbase"," and ",[46,3151,3152],{},"branch",", and replay them onto ",[46,3155,3156],{},"newbase",[32,3158,3160],{"id":3159},"scenario-1-moving-a-branch-to-a-different-parent","Scenario 1: Moving a Branch to a Different Parent",[15,3162,3163,3164,3167,3168,3171,3172,3174,3175,3177,3178,2416],{},"You branched ",[46,3165,3166],{},"feature-b"," off ",[46,3169,3170],{},"feature-a",", but now ",[46,3173,3170],{}," is abandoned and you want ",[46,3176,3166],{}," based on ",[46,3179,3180],{},"main",[51,3182,3184],{"className":53,"code":3183,"language":55,"meta":56,"style":56},"# Before:\n# main -- A -- B (feature-a) -- C -- D (feature-b)\n\ngit rebase --onto main feature-a feature-b\n\n# After:\n# main -- C' -- D' (feature-b)\n",[46,3185,3186,3191,3196,3200,3217,3221,3226],{"__ignoreMap":56},[60,3187,3188],{"class":62,"line":63},[60,3189,3190],{"class":127},"# Before:\n",[60,3192,3193],{"class":62,"line":131},[60,3194,3195],{"class":127},"# main -- A -- B (feature-a) -- C -- D (feature-b)\n",[60,3197,3198],{"class":62,"line":151},[60,3199,155],{"emptyLinePlaceholder":154},[60,3201,3202,3204,3206,3208,3211,3214],{"class":62,"line":158},[60,3203,3104],{"class":134},[60,3205,3107],{"class":77},[60,3207,3110],{"class":138},[60,3209,3210],{"class":77}," main",[60,3212,3213],{"class":77}," feature-a",[60,3215,3216],{"class":77}," feature-b\n",[60,3218,3219],{"class":62,"line":164},[60,3220,155],{"emptyLinePlaceholder":154},[60,3222,3223],{"class":62,"line":180},[60,3224,3225],{"class":127},"# After:\n",[60,3227,3228],{"class":62,"line":185},[60,3229,3230],{"class":127},"# main -- C' -- D' (feature-b)\n",[15,3232,3233,3234,3236,3237,955],{},"Only commits C and D — the ones unique to ",[46,3235,3166],{}," — are replayed onto ",[46,3238,3180],{},[32,3240,3242],{"id":3241},"scenario-2-removing-commits-from-the-middle","Scenario 2: Removing Commits from the Middle",[15,3244,3245],{},"You made commits A, B, C, D on a branch but B and C were a wrong turn:",[51,3247,3249],{"className":53,"code":3248,"language":55,"meta":56,"style":56},"git rebase --onto A C feature-branch\n\n# Before: main -- A -- B -- C -- D (feature-branch)\n# After:  main -- A -- D' (feature-branch)\n",[46,3250,3251,3268,3272,3277],{"__ignoreMap":56},[60,3252,3253,3255,3257,3259,3262,3265],{"class":62,"line":63},[60,3254,3104],{"class":134},[60,3256,3107],{"class":77},[60,3258,3110],{"class":138},[60,3260,3261],{"class":77}," A",[60,3263,3264],{"class":77}," C",[60,3266,3267],{"class":77}," feature-branch\n",[60,3269,3270],{"class":62,"line":131},[60,3271,155],{"emptyLinePlaceholder":154},[60,3273,3274],{"class":62,"line":151},[60,3275,3276],{"class":127},"# Before: main -- A -- B -- C -- D (feature-branch)\n",[60,3278,3279],{"class":62,"line":158},[60,3280,3281],{"class":127},"# After:  main -- A -- D' (feature-branch)\n",[15,3283,3284],{},"Commits B and C are cleanly removed. D is replayed directly onto A.",[32,3286,3288],{"id":3287},"scenario-3-splitting-a-stacked-pr","Scenario 3: Splitting a Stacked PR",[15,3290,3291],{},"You have a branch with 6 commits but only the first 3 should be in PR #1:",[51,3293,3295],{"className":53,"code":3294,"language":55,"meta":56,"style":56},"git checkout -b pr-2 feature-branch\ngit rebase --onto $(git rev-parse HEAD~3) feature-branch~3 pr-2\n",[46,3296,3297,3312],{"__ignoreMap":56},[60,3298,3299,3301,3304,3307,3310],{"class":62,"line":63},[60,3300,3104],{"class":134},[60,3302,3303],{"class":77}," checkout",[60,3305,3306],{"class":138}," -b",[60,3308,3309],{"class":77}," pr-2",[60,3311,3267],{"class":77},[60,3313,3314,3316,3318,3320,3323,3325,3328,3331,3334,3337],{"class":62,"line":131},[60,3315,3104],{"class":134},[60,3317,3107],{"class":77},[60,3319,3110],{"class":138},[60,3321,3322],{"class":70}," $(",[60,3324,3104],{"class":134},[60,3326,3327],{"class":77}," rev-parse",[60,3329,3330],{"class":77}," HEAD~3",[60,3332,3333],{"class":70},") ",[60,3335,3336],{"class":77},"feature-branch~3",[60,3338,3339],{"class":77}," pr-2\n",[15,3341,98,3342,3345],{},[46,3343,3344],{},"pr-2"," contains only the last 3 commits, based on the tip of what will be PR #1.",[32,3347,602],{"id":601},[15,3349,3350,3351,3354,3355,3358],{},"Always use ",[46,3352,3353],{},"git log --oneline --graph"," before and after a ",[46,3356,3357],{},"rebase --onto"," operation. The three-argument form is powerful but easy to get wrong if you miscounted commits or picked the wrong base.",[15,3360,3361,3362,3365,3366,3369],{},"If something goes wrong: ",[46,3363,3364],{},"git reflog"," is your safety net. Every rebase records the pre-rebase HEAD, so you can always ",[46,3367,3368],{},"git reset --hard"," back to it.",[32,3371,1394],{"id":1393},[15,3373,3374],{},"Real-world scenario — you started a feature branch off a release branch, but now need it on main:",[51,3376,3378],{"className":53,"code":3377,"language":55,"meta":56,"style":56},"# See where your branch diverged\ngit log --oneline --graph --all\n\n# Count: release-2.1 is 12 commits ahead of main\n# Your feature has 4 commits on top of release-2.1\n\ngit rebase --onto main release-2.1 my-feature\n\n# Verify\ngit log --oneline main..my-feature\n# Should show exactly your 4 commits\n",[46,3379,3380,3385,3401,3405,3410,3415,3419,3435,3439,3444,3455],{"__ignoreMap":56},[60,3381,3382],{"class":62,"line":63},[60,3383,3384],{"class":127},"# See where your branch diverged\n",[60,3386,3387,3389,3392,3395,3398],{"class":62,"line":131},[60,3388,3104],{"class":134},[60,3390,3391],{"class":77}," log",[60,3393,3394],{"class":138}," --oneline",[60,3396,3397],{"class":138}," --graph",[60,3399,3400],{"class":138}," --all\n",[60,3402,3403],{"class":62,"line":151},[60,3404,155],{"emptyLinePlaceholder":154},[60,3406,3407],{"class":62,"line":158},[60,3408,3409],{"class":127},"# Count: release-2.1 is 12 commits ahead of main\n",[60,3411,3412],{"class":62,"line":164},[60,3413,3414],{"class":127},"# Your feature has 4 commits on top of release-2.1\n",[60,3416,3417],{"class":62,"line":180},[60,3418,155],{"emptyLinePlaceholder":154},[60,3420,3421,3423,3425,3427,3429,3432],{"class":62,"line":185},[60,3422,3104],{"class":134},[60,3424,3107],{"class":77},[60,3426,3110],{"class":138},[60,3428,3210],{"class":77},[60,3430,3431],{"class":77}," release-2.1",[60,3433,3434],{"class":77}," my-feature\n",[60,3436,3437],{"class":62,"line":191},[60,3438,155],{"emptyLinePlaceholder":154},[60,3440,3441],{"class":62,"line":207},[60,3442,3443],{"class":127},"# Verify\n",[60,3445,3446,3448,3450,3452],{"class":62,"line":212},[60,3447,3104],{"class":134},[60,3449,3391],{"class":77},[60,3451,3394],{"class":138},[60,3453,3454],{"class":77}," main..my-feature\n",[60,3456,3457],{"class":62,"line":218},[60,3458,3459],{"class":127},"# Should show exactly your 4 commits\n",[15,3461,3462,3463,3465,3466,3149,3468,3470],{},"The key insight: the second argument (",[46,3464,3148],{},") defines the boundary. Everything between ",[46,3467,3148],{},[46,3469,3152],{}," is what moves.",[1061,3472,3473],{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":56,"searchDepth":131,"depth":131,"links":3475},[3476,3477,3478,3479,3480,3481],{"id":3093,"depth":131,"text":3094},{"id":3159,"depth":131,"text":3160},{"id":3241,"depth":131,"text":3242},{"id":3287,"depth":131,"text":3288},{"id":601,"depth":131,"text":602},{"id":1393,"depth":131,"text":1394},"2026-03-26","Standard git rebase replays your branch commits on top of another branch. But git rebase --onto gives you a third argument — and with it, surgical control over exactly which commits go where.","advanced","walkthrough",{},"\u002Flessons\u002F2026-03-26",{"title":3077,"description":3483},"lessons\u002F2026-03-26","rebase-onto",[3104,3492,3493,3494],"rebase","branching","workflow","git rebase --onto lets you transplant a branch to any base — the surgical precision tool most developers never learn.","cvDfWYL-1YFs3A5l9b0QlNx_4g1d3fvUULOnIMj4hKQ",{"id":3498,"title":3499,"body":3500,"date":3773,"description":3774,"difficulty":1075,"extension":1076,"format":1077,"meta":3775,"navigation":154,"path":3776,"progression":131,"seo":3777,"stem":3778,"subtopic":1478,"tags":3779,"tldr":3781,"topic":1133,"triggered":2096,"__hash__":3782},"lessons\u002Flessons\u002F2026-03-25.md","Registers: Vim's Hidden Clipboard System",{"type":8,"value":3501,"toc":3766},[3502,3511,3515,3518,3575,3579,3590,3610,3613,3648,3652,3658,3669,3671,3677,3686,3703,3705,3708,3761,3764],[15,3503,3504,3505,3149,3508,3510],{},"Most Vim users know ",[46,3506,3507],{},"y",[46,3509,15],{},". Fewer know that every yank and delete operation writes to a register — and that you can control exactly which one.",[32,3512,3514],{"id":3513},"the-register-landscape","The Register Landscape",[15,3516,3517],{},"Vim maintains several categories of registers:",[1210,3519,3520,3530,3536,3545,3551,3557,3563,3569],{},[1166,3521,3522,3525,3526,3529],{},[46,3523,3524],{},"\"a"," through ",[46,3527,3528],{},"\"z"," — 26 named registers you control explicitly",[1166,3531,3532,3535],{},[46,3533,3534],{},"\"0"," — the yank register (last yanked text, untouched by deletes)",[1166,3537,3538,3525,3541,3544],{},[46,3539,3540],{},"\"1",[46,3542,3543],{},"\"9"," — the delete history (a stack of your last 9 deletes)",[1166,3546,3547,3550],{},[46,3548,3549],{},"\"+"," — the system clipboard",[1166,3552,3553,3556],{},[46,3554,3555],{},"\"_"," — the black hole register (deletes without saving)",[1166,3558,3559,3562],{},[46,3560,3561],{},"\"."," — the last inserted text",[1166,3564,3565,3568],{},[46,3566,3567],{},"\"%"," — the current filename",[1166,3570,3571,3574],{},[46,3572,3573],{},"\":"," — the last ex command",[32,3576,3578],{"id":3577},"using-named-registers","Using Named Registers",[15,3580,3581,3582,3585,3586,3589],{},"Prefix any yank or delete with ",[46,3583,3584],{},"\"x"," where ",[46,3587,3588],{},"x"," is the register letter:",[51,3591,3593],{"className":1131,"code":3592,"language":1133,"meta":56,"style":56},"\"ayy    \" yank current line into register a\n\"Ayy    \" append current line to register a (uppercase = append)\n\"ap     \" paste from register a\n",[46,3594,3595,3600,3605],{"__ignoreMap":56},[60,3596,3597],{"class":62,"line":63},[60,3598,3599],{},"\"ayy    \" yank current line into register a\n",[60,3601,3602],{"class":62,"line":131},[60,3603,3604],{},"\"Ayy    \" append current line to register a (uppercase = append)\n",[60,3606,3607],{"class":62,"line":151},[60,3608,3609],{},"\"ap     \" paste from register a\n",[15,3611,3612],{},"The uppercase trick is particularly powerful. Build up a collection of lines from different parts of a file by appending to the same register:",[51,3614,3616],{"className":1131,"code":3615,"language":1133,"meta":56,"style":56},"\"ayy    \" first line into register a\njj\n\"Ayy    \" another line appended to a\n5G\n\"Ayy    \" yet another line appended\n\"ap     \" paste all three lines together\n",[46,3617,3618,3623,3628,3633,3638,3643],{"__ignoreMap":56},[60,3619,3620],{"class":62,"line":63},[60,3621,3622],{},"\"ayy    \" first line into register a\n",[60,3624,3625],{"class":62,"line":131},[60,3626,3627],{},"jj\n",[60,3629,3630],{"class":62,"line":151},[60,3631,3632],{},"\"Ayy    \" another line appended to a\n",[60,3634,3635],{"class":62,"line":158},[60,3636,3637],{},"5G\n",[60,3639,3640],{"class":62,"line":164},[60,3641,3642],{},"\"Ayy    \" yet another line appended\n",[60,3644,3645],{"class":62,"line":180},[60,3646,3647],{},"\"ap     \" paste all three lines together\n",[32,3649,3651],{"id":3650},"the-yank-register-trick","The Yank Register Trick",[15,3653,3654,3655,3657],{},"Here's the scenario: you yank a line, then delete something to make room, then try to paste — but ",[46,3656,15],{}," pastes the deleted text, not the yanked line.",[15,3659,3660,3661,3664,3665,3668],{},"The fix: ",[46,3662,3663],{},"\"0p",". Register ",[46,3666,3667],{},"0"," always holds your last yank, unaffected by delete operations.",[32,3670,602],{"id":601},[15,3672,1896,3673,3676],{},[46,3674,3675],{},":reg"," to see the contents of all registers at any time. Better yet, map it:",[51,3678,3680],{"className":1131,"code":3679,"language":1133,"meta":56,"style":56},"nnoremap \u003Cleader>r :registers\u003CCR>\n",[46,3681,3682],{"__ignoreMap":56},[60,3683,3684],{"class":62,"line":63},[60,3685,3679],{},[15,3687,3688,3689,3691,3692,3694,3695,3698,3699,3702],{},"When you're refactoring, use named registers as staging areas. Yank the replacement text into ",[46,3690,3524],{},", then navigate and use ",[46,3693,1216],{}," followed by ",[46,3696,3697],{},"Ctrl-R a"," in insert mode to paste from register ",[46,3700,3701],{},"a"," without leaving insert mode.",[32,3704,1394],{"id":1393},[15,3706,3707],{},"A real workflow for renaming a variable across a function:",[51,3709,3711],{"className":1131,"code":3710,"language":1133,"meta":56,"style":56},"\" Yank the new name into register n\n\u002FnewVariableName\n\"nyiw\n\n\" Now find each old name and replace\n\u002FoldVariableName\nciw\u003CC-r>n\u003CEsc>\n\n\" Repeat with n (find next) and . (repeat change)\nn.n.n.\n",[46,3712,3713,3718,3723,3728,3732,3737,3742,3747,3751,3756],{"__ignoreMap":56},[60,3714,3715],{"class":62,"line":63},[60,3716,3717],{},"\" Yank the new name into register n\n",[60,3719,3720],{"class":62,"line":131},[60,3721,3722],{},"\u002FnewVariableName\n",[60,3724,3725],{"class":62,"line":151},[60,3726,3727],{},"\"nyiw\n",[60,3729,3730],{"class":62,"line":158},[60,3731,155],{"emptyLinePlaceholder":154},[60,3733,3734],{"class":62,"line":164},[60,3735,3736],{},"\" Now find each old name and replace\n",[60,3738,3739],{"class":62,"line":180},[60,3740,3741],{},"\u002FoldVariableName\n",[60,3743,3744],{"class":62,"line":185},[60,3745,3746],{},"ciw\u003CC-r>n\u003CEsc>\n",[60,3748,3749],{"class":62,"line":191},[60,3750,155],{"emptyLinePlaceholder":154},[60,3752,3753],{"class":62,"line":207},[60,3754,3755],{},"\" Repeat with n (find next) and . (repeat change)\n",[60,3757,3758],{"class":62,"line":212},[60,3759,3760],{},"n.n.n.\n",[15,3762,3763],{},"This is faster than a substitution command when you want to review each replacement individually.",[1061,3765,1457],{},{"title":56,"searchDepth":131,"depth":131,"links":3767},[3768,3769,3770,3771,3772],{"id":3513,"depth":131,"text":3514},{"id":3577,"depth":131,"text":3578},{"id":3650,"depth":131,"text":3651},{"id":601,"depth":131,"text":602},{"id":1393,"depth":131,"text":1394},"2026-03-25","Most Vim users know y and p. Fewer know that every yank and delete operation writes to a register — and that you can control exactly which one.",{},"\u002Flessons\u002F2026-03-25",{"title":3499,"description":3774},"lessons\u002F2026-03-25",[1133,1478,3780,1479],"clipboard","Vim has 26+ named registers that act like individual clipboards. Master them and you'll never lose a yanked line again.","pIwKA40SKrLQKN-fA1DJzRM99p9S4wlurRjOfnHBUZY",1775249073929]