[{"data":1,"prerenderedAt":756},["ShallowReactive",2],{"lesson-2026-03-27":3},{"id":4,"title":5,"body":6,"date":739,"description":740,"difficulty":741,"extension":742,"format":743,"meta":744,"navigation":184,"path":745,"progression":54,"seo":746,"stem":747,"subtopic":748,"tags":749,"tldr":752,"topic":753,"triggered":754,"__hash__":755},"lessons\u002Flessons\u002F2026-03-27.md","Shell Parameter Expansion: Beyond ${var}",{"type":7,"value":8,"toc":729},"minimark",[9,18,23,93,100,104,159,163,285,289,361,365,412,416,445,449,452,494,497,566,570,573,711,725],[10,11,12,13,17],"p",{},"Every time you write ",[14,15,16],"code",{},"$(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.",[19,20,22],"h2",{"id":21},"default-values","Default Values",[24,25,30],"pre",{"className":26,"code":27,"language":28,"meta":29,"style":29},"language-bash shiki shiki-themes github-light","${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","bash","",[14,31,32,52,65,79],{"__ignoreMap":29},[33,34,37,41,45,48],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sgsFI","${var",[33,42,44],{"class":43},"sD7c4",":-",[33,46,47],{"class":39},"default}    ",[33,49,51],{"class":50},"sAwPA","# Use default if var is unset or empty\n",[33,53,55,57,60,62],{"class":35,"line":54},2,[33,56,40],{"class":39},[33,58,59],{"class":43},":=",[33,61,47],{"class":39},[33,63,64],{"class":50},"# Assign default if var is unset or empty\n",[33,66,68,70,73,76],{"class":35,"line":67},3,[33,69,40],{"class":39},[33,71,72],{"class":43},":",[33,74,75],{"class":39},"+alternate}  ",[33,77,78],{"class":50},"# Use alternate if var IS set and non-empty\n",[33,80,82,84,87,90],{"class":35,"line":81},4,[33,83,40],{"class":39},[33,85,86],{"class":43},":?",[33,88,89],{"class":39},"error msg}  ",[33,91,92],{"class":50},"# Exit with error if var is unset or empty\n",[10,94,95,96,99],{},"The colon matters. Without it (",[14,97,98],{},"${var-default}","), only checks if unset, not empty.",[19,101,103],{"id":102},"string-manipulation","String Manipulation",[24,105,107],{"className":26,"code":106,"language":28,"meta":29,"style":29},"${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",[14,108,109,122,135,147],{"__ignoreMap":29},[33,110,111,113,116,119],{"class":35,"line":36},[33,112,40],{"class":39},[33,114,115],{"class":43},"#",[33,117,118],{"class":39},"pattern}     ",[33,120,121],{"class":50},"# Remove shortest match from beginning\n",[33,123,124,126,129,132],{"class":35,"line":54},[33,125,40],{"class":39},[33,127,128],{"class":43},"##",[33,130,131],{"class":39},"pattern}    ",[33,133,134],{"class":50},"# Remove longest match from beginning\n",[33,136,137,139,142,144],{"class":35,"line":67},[33,138,40],{"class":39},[33,140,141],{"class":43},"%",[33,143,118],{"class":39},[33,145,146],{"class":50},"# Remove shortest match from end\n",[33,148,149,151,154,156],{"class":35,"line":81},[33,150,40],{"class":39},[33,152,153],{"class":43},"%%",[33,155,131],{"class":39},[33,157,158],{"class":50},"# Remove longest match from end\n",[19,160,162],{"id":161},"real-examples","Real Examples",[24,164,166],{"className":26,"code":165,"language":28,"meta":29,"style":29},"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",[14,167,168,180,186,200,213,218,236,255,271],{"__ignoreMap":29},[33,169,170,173,176],{"class":35,"line":36},[33,171,172],{"class":39},"filepath",[33,174,175],{"class":43},"=",[33,177,179],{"class":178},"sYBdl","\"\u002Fhome\u002Fuser\u002Fdocuments\u002Freport.tar.gz\"\n",[33,181,182],{"class":35,"line":54},[33,183,185],{"emptyLinePlaceholder":184},true,"\n",[33,187,188,191,194,197],{"class":35,"line":67},[33,189,190],{"class":39},"${filepath",[33,192,193],{"class":43},"##*\u002F",[33,195,196],{"class":39},"}      ",[33,198,199],{"class":50},"# report.tar.gz  (basename)\n",[33,201,202,204,207,210],{"class":35,"line":81},[33,203,190],{"class":39},[33,205,206],{"class":43},"%\u002F*",[33,208,209],{"class":39},"}       ",[33,211,212],{"class":50},"# \u002Fhome\u002Fuser\u002Fdocuments  (dirname)\n",[33,214,216],{"class":35,"line":215},5,[33,217,185],{"emptyLinePlaceholder":184},[33,219,221,224,226,229,231,233],{"class":35,"line":220},6,[33,222,223],{"class":39},"filename",[33,225,175],{"class":43},[33,227,228],{"class":178},"\"${",[33,230,172],{"class":39},[33,232,193],{"class":43},[33,234,235],{"class":178},"}\"\n",[33,237,239,242,244,247,250,252],{"class":35,"line":238},7,[33,240,241],{"class":39},"${filename",[33,243,153],{"class":43},[33,245,246],{"class":39},".",[33,248,249],{"class":43},"*",[33,251,196],{"class":39},[33,253,254],{"class":50},"# report  (name without any extension)\n",[33,256,258,260,262,264,266,268],{"class":35,"line":257},8,[33,259,241],{"class":39},[33,261,141],{"class":43},[33,263,246],{"class":39},[33,265,249],{"class":43},[33,267,209],{"class":39},[33,269,270],{"class":50},"# report.tar  (name without last extension)\n",[33,272,274,276,279,282],{"class":35,"line":273},9,[33,275,241],{"class":39},[33,277,278],{"class":43},"##*",[33,280,281],{"class":39},".}      ",[33,283,284],{"class":50},"# gz  (last extension only)\n",[19,286,288],{"id":287},"substitution","Substitution",[24,290,292],{"className":26,"code":291,"language":28,"meta":29,"style":29},"${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",[14,293,294,312,329,345],{"__ignoreMap":29},[33,295,296,298,301,304,306,309],{"class":35,"line":36},[33,297,40],{"class":39},[33,299,300],{"class":43},"\u002F",[33,302,303],{"class":39},"pattern",[33,305,300],{"class":43},[33,307,308],{"class":39},"replacement}   ",[33,310,311],{"class":50},"# Replace first match\n",[33,313,314,316,319,321,323,326],{"class":35,"line":54},[33,315,40],{"class":39},[33,317,318],{"class":43},"\u002F\u002F",[33,320,303],{"class":39},[33,322,300],{"class":43},[33,324,325],{"class":39},"replacement}  ",[33,327,328],{"class":50},"# Replace all matches\n",[33,330,331,333,336,338,340,342],{"class":35,"line":67},[33,332,40],{"class":39},[33,334,335],{"class":43},"\u002F#",[33,337,303],{"class":39},[33,339,300],{"class":43},[33,341,325],{"class":39},[33,343,344],{"class":50},"# Replace if at beginning\n",[33,346,347,349,352,354,356,358],{"class":35,"line":81},[33,348,40],{"class":39},[33,350,351],{"class":43},"\u002F%",[33,353,303],{"class":39},[33,355,300],{"class":43},[33,357,325],{"class":39},[33,359,360],{"class":50},"# Replace if at end\n",[19,362,364],{"id":363},"length-and-slicing","Length and Slicing",[24,366,368],{"className":26,"code":367,"language":28,"meta":29,"style":29},"${#var}              # String length\n${var:offset}        # Substring from offset\n${var:offset:length} # Substring with length\n",[14,369,370,383,395],{"__ignoreMap":29},[33,371,372,375,377,380],{"class":35,"line":36},[33,373,374],{"class":39},"${",[33,376,115],{"class":43},[33,378,379],{"class":39},"var}              ",[33,381,382],{"class":50},"# String length\n",[33,384,385,387,389,392],{"class":35,"line":54},[33,386,40],{"class":39},[33,388,72],{"class":43},[33,390,391],{"class":39},"offset}        ",[33,393,394],{"class":50},"# Substring from offset\n",[33,396,397,399,401,404,406,409],{"class":35,"line":67},[33,398,40],{"class":39},[33,400,72],{"class":43},[33,402,403],{"class":39},"offset",[33,405,72],{"class":43},[33,407,408],{"class":39},"length} ",[33,410,411],{"class":50},"# Substring with length\n",[19,413,415],{"id":414},"case-conversion-bash-4","Case Conversion (Bash 4+)",[24,417,419],{"className":26,"code":418,"language":28,"meta":29,"style":29},"${var^^}    # UPPERCASE\n${var,,}    # lowercase\n${var^}     # Capitalize first letter\n",[14,420,421,429,437],{"__ignoreMap":29},[33,422,423,426],{"class":35,"line":36},[33,424,425],{"class":39},"${var^^}    ",[33,427,428],{"class":50},"# UPPERCASE\n",[33,430,431,434],{"class":35,"line":54},[33,432,433],{"class":39},"${var,,}    ",[33,435,436],{"class":50},"# lowercase\n",[33,438,439,442],{"class":35,"line":67},[33,440,441],{"class":39},"${var^}     ",[33,443,444],{"class":50},"# Capitalize first letter\n",[19,446,448],{"id":447},"pro-tip","Pro Tip",[10,450,451],{},"Combine expansions for powerful one-liners. Convert a filename from snake_case to kebab-case without any external tools:",[24,453,455],{"className":26,"code":454,"language":28,"meta":29,"style":29},"file=\"my_cool_script.sh\"\necho \"${file\u002F\u002F_\u002F-}\"   # my-cool-script.sh\n",[14,456,457,467],{"__ignoreMap":29},[33,458,459,462,464],{"class":35,"line":36},[33,460,461],{"class":39},"file",[33,463,175],{"class":43},[33,465,466],{"class":178},"\"my_cool_script.sh\"\n",[33,468,469,473,476,478,480,483,485,488,491],{"class":35,"line":54},[33,470,472],{"class":471},"sYu0t","echo",[33,474,475],{"class":178}," \"${",[33,477,461],{"class":39},[33,479,318],{"class":43},[33,481,482],{"class":39},"_",[33,484,300],{"class":43},[33,486,487],{"class":39},"-",[33,489,490],{"class":178},"}\"",[33,492,493],{"class":50},"   # my-cool-script.sh\n",[10,495,496],{},"Or strip a common prefix from all files in a directory:",[24,498,500],{"className":26,"code":499,"language":28,"meta":29,"style":29},"prefix=\"project_v2_\"\nfor f in ${prefix}*; do\n  mv \"$f\" \"${f#$prefix}\"\ndone\n",[14,501,502,512,534,561],{"__ignoreMap":29},[33,503,504,507,509],{"class":35,"line":36},[33,505,506],{"class":39},"prefix",[33,508,175],{"class":43},[33,510,511],{"class":178},"\"project_v2_\"\n",[33,513,514,517,520,523,526,528,531],{"class":35,"line":54},[33,515,516],{"class":43},"for",[33,518,519],{"class":39}," f ",[33,521,522],{"class":43},"in",[33,524,525],{"class":39}," ${prefix}",[33,527,249],{"class":178},[33,529,530],{"class":39},"; ",[33,532,533],{"class":43},"do\n",[33,535,536,540,543,546,549,551,554,556,559],{"class":35,"line":67},[33,537,539],{"class":538},"s7eDp","  mv",[33,541,542],{"class":178}," \"",[33,544,545],{"class":39},"$f",[33,547,548],{"class":178},"\"",[33,550,475],{"class":178},[33,552,553],{"class":39},"f",[33,555,115],{"class":43},[33,557,558],{"class":39},"$prefix",[33,560,235],{"class":178},[33,562,563],{"class":35,"line":81},[33,564,565],{"class":43},"done\n",[19,567,569],{"id":568},"example","Example",[10,571,572],{},"A practical script that processes log file paths — extracting date, service name, and rotating based on age — using nothing but parameter expansion:",[24,574,576],{"className":26,"code":575,"language":28,"meta":29,"style":29},"#!\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",[14,577,578,583,599,618,641,661,683,687,707],{"__ignoreMap":29},[33,579,580],{"class":35,"line":36},[33,581,582],{"class":50},"#!\u002Fbin\u002Fbash\n",[33,584,585,587,590,592,595,597],{"class":35,"line":54},[33,586,516],{"class":43},[33,588,589],{"class":39}," logfile ",[33,591,522],{"class":43},[33,593,594],{"class":178}," \u002Fvar\u002Flog\u002Fservices\u002F*.2026-*.log",[33,596,530],{"class":39},[33,598,533],{"class":43},[33,600,601,604,606,608,611,613,615],{"class":35,"line":67},[33,602,603],{"class":39},"  base",[33,605,175],{"class":43},[33,607,228],{"class":178},[33,609,610],{"class":39},"logfile",[33,612,193],{"class":43},[33,614,490],{"class":178},[33,616,617],{"class":50},"             # api-gateway.2026-03-15.log\n",[33,619,620,623,625,627,630,632,634,636,638],{"class":35,"line":81},[33,621,622],{"class":39},"  service",[33,624,175],{"class":43},[33,626,228],{"class":178},[33,628,629],{"class":39},"base",[33,631,153],{"class":43},[33,633,246],{"class":178},[33,635,249],{"class":43},[33,637,490],{"class":178},[33,639,640],{"class":50},"            # api-gateway\n",[33,642,643,646,648,650,652,655,658],{"class":35,"line":215},[33,644,645],{"class":39},"  date_part",[33,647,175],{"class":43},[33,649,228],{"class":178},[33,651,629],{"class":39},[33,653,654],{"class":43},"#*",[33,656,657],{"class":178},".}\"",[33,659,660],{"class":50},"           # 2026-03-15.log\n",[33,662,663,665,667,669,672,674,676,678,680],{"class":35,"line":220},[33,664,645],{"class":39},[33,666,175],{"class":43},[33,668,228],{"class":178},[33,670,671],{"class":39},"date_part",[33,673,141],{"class":43},[33,675,246],{"class":178},[33,677,249],{"class":43},[33,679,490],{"class":178},[33,681,682],{"class":50},"      # 2026-03-15\n",[33,684,685],{"class":35,"line":238},[33,686,185],{"emptyLinePlaceholder":184},[33,688,689,692,695,698,701,704],{"class":35,"line":257},[33,690,691],{"class":471},"  echo",[33,693,694],{"class":178}," \"Service: ",[33,696,697],{"class":39},"$service",[33,699,700],{"class":178},", Date: ",[33,702,703],{"class":39},"$date_part",[33,705,706],{"class":178},"\"\n",[33,708,709],{"class":35,"line":273},[33,710,565],{"class":43},[10,712,713,714,717,718,717,721,724],{},"No ",[14,715,716],{},"awk",". No ",[14,719,720],{},"sed",[14,722,723],{},"cut",". Just the shell doing what it was designed to do.",[726,727,728],"style",{},"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":29,"searchDepth":54,"depth":54,"links":730},[731,732,733,734,735,736,737,738],{"id":21,"depth":54,"text":22},{"id":102,"depth":54,"text":103},{"id":161,"depth":54,"text":162},{"id":287,"depth":54,"text":288},{"id":363,"depth":54,"text":364},{"id":414,"depth":54,"text":415},{"id":447,"depth":54,"text":448},{"id":568,"depth":54,"text":569},"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.","intermediate","md","cheatsheet",{},"\u002Flessons\u002F2026-03-27",{"title":5,"description":740},"lessons\u002F2026-03-27","parameter-expansion",[28,750,748,751],"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",false,"oXaRIcLDNOFw16Ze5aSwfMatBLD24AyyISCuhjITzKI",1775249074012]