[{"data":1,"prerenderedAt":1552},["ShallowReactive",2],{"blog-de-wie-weiss-ich-wann-bash-fertig-ist":3,"header-blog-translations-/de/blog/wie-weiss-ich-wann-bash-fertig-ist":1549},{"id":4,"title":5,"author":6,"body":7,"date":1532,"description":1533,"draft":1534,"extension":1535,"image":1536,"meta":1537,"navigation":322,"path":1538,"seo":1539,"stem":1540,"tags":1541,"translationKey":1547,"__hash__":1548},"blog_de/blog/de/wie-weiss-ich-wann-bash-fertig-ist.md","Wie weiß ich, wann bash fertig ist?","Patrick Hofmann",{"type":8,"value":9,"toc":1512},"minimark",[10,22,29,34,45,50,53,63,66,102,113,119,123,128,134,145,149,156,160,163,170,176,183,187,201,205,208,219,223,226,232,235,247,251,255,270,277,367,370,376,391,395,398,411,425,698,701,735,739,742,807,822,826,829,1232,1247,1251,1265,1268,1285,1291,1460,1463,1467,1487,1498,1502,1505,1508],[11,12,13,14,18,19,21],"p",{},"Gestern habe ich hier über ",[15,16,17],"code",{},"ape-shell"," geschrieben — einen Shell-Wrapper der Commands durch ein Grant-System routet, bevor sie ausgeführt werden. Die ursprüngliche Version war ein reiner One-Shot-Modus: du gibst einen Command rein, ",[15,20,17],{}," holt sich einen Grant, bash führt aus, fertig. Jeder Command bekam eine frische bash-Instanz.",[11,23,24,25,28],{},"Das funktioniert für ",[15,26,27],{},"$SHELL -c","-Patterns. Es funktioniert nicht für interaktive Sessions. Und der Grund dafür ist eine Frage die auf den ersten Blick trivial wirkt und sich dann als überraschend tief erweist:",[11,30,31],{},[32,33,5],"strong",{},[11,35,36,37,39,40,44],{},"Dieser Artikel ist die Antwort. Kein ",[15,38,17],{},"-Pitch, keine Grant-System-Diskussion — nur ein konkretes Systems-Programming-Problem und wie man es löst. Wenn du jemals versucht hast, einen Shell-Wrapper zu bauen der eine ",[41,42,43],"em",{},"persistente"," bash über mehrere Commands hinweg kontrolliert, hast du wahrscheinlich dieselbe Frage gestellt.",[46,47,49],"h2",{"id":48},"das-problem","Das Problem",[11,51,52],{},"Die naive Variante eines Shell-Wrappers ist einfach:",[54,55,61],"pre",{"className":56,"code":58,"language":59,"meta":60},[57],"language-text","spawn bash\nwrite command\nread output\nkill bash\n","text","",[15,62,58],{"__ignoreMap":60},[11,64,65],{},"Das funktioniert, ist aber für viele Use-Cases nutzlos. Denn wenn jeder Command eine neue bash-Instanz bekommt, geht zwischen Commands der Shell-State verloren:",[67,68,69,76,86,92,95],"ul",{},[70,71,72,75],"li",{},[15,73,74],{},"cd /foo"," im ersten Command → im zweiten Command bist du wieder im alten Verzeichnis",[70,77,78,81,82,85],{},[15,79,80],{},"export FOO=bar"," → im nächsten Command ist ",[15,83,84],{},"$FOO"," leer",[70,87,88,91],{},[15,89,90],{},"alias ll='ls -la'"," → weg",[70,93,94],{},"Shell-Funktionen → weg",[70,96,97,98,101],{},"Geladene ",[15,99,100],{},".bashrc","-Konfiguration → gelesen, dann zusammen mit der bash beerdigt",[11,103,104,105,108,109,112],{},"Für einen Wrapper der sich wie eine ",[41,106,107],{},"echte"," Shell anfühlen soll, ist das eine Sackgasse. Du brauchst ",[32,110,111],{},"eine"," bash die lebt, und du schiebst Commands rein.",[11,114,115,116],{},"Dann stellt sich die Frage: ",[41,117,118],{},"wann ist bash mit dem aktuellen Command durch und bereit für den nächsten?",[46,120,122],{"id":121},"naive-ansätze-und-warum-sie-brechen","Naive Ansätze und warum sie brechen",[124,125,127],"h3",{"id":126},"ansatz-1-wart-mal-kurz","Ansatz 1: Wart mal kurz",[54,129,132],{"className":130,"code":131,"language":59,"meta":60},[57],"write command\nsleep 500ms\nread whatever accumulated\n",[15,133,131],{"__ignoreMap":60},[11,135,136,137,140,141,144],{},"Bricht sofort. Was passiert bei ",[15,138,139],{},"find / -name '*.log'","? Das läuft Minuten. Was passiert bei ",[15,142,143],{},"yes | head -n 1000000","? Das spuckt Megabytes in Millisekunden aus und wird garantiert über die 500ms hinausgehen. Timeouts sind keine Antwort auf eine Frage über Semantik.",[124,146,148],{"id":147},"ansatz-2-auf-newlines-warten","Ansatz 2: Auf Newlines warten",[11,150,151,152,155],{},"\"Wenn 200ms keine neue Newline kam, ist bash fertig.\" Auch nicht. ",[15,153,154],{},"tail -f log.txt"," sendet gelegentlich Zeilen, dann Pausen, dann wieder Zeilen. Newline-basierte Heuristiken produzieren flaky Ergebnisse die bei jedem zehnten Aufruf anders aussehen.",[124,157,159],{"id":158},"ansatz-3-auf-den-prompt-warten","Ansatz 3: Auf den Prompt warten",[11,161,162],{},"Bash zeigt nach jedem Command einen Prompt. Wenn du den Prompt siehst, ist bash fertig. Logisch, oder?",[11,164,165,166,169],{},"Nur: ",[32,167,168],{},"welchen Prompt?"," Die User-PS1 ist frei konfigurierbar. Bei mir sieht sie ungefähr so aus:",[54,171,174],{"className":172,"code":173,"language":59,"meta":60},[57],"patrick@mbp ~/code/openape (main *) $ \n",[15,175,173],{"__ignoreMap":60},[11,177,178,179,182],{},"Mit ANSI-Farben, mit Git-Branch-Info, mit einem Dirty-State-Marker, mit Unicode-Dekoration. Manchmal mehrzeilig. Manchmal mit einem Zeilenvorschub davor. Ein Parser der ",[41,180,181],{},"beliebige"," User-PS1 erkennen will, ist zum Scheitern verurteilt.",[124,184,186],{"id":185},"ansatz-4-ps1-parsen","Ansatz 4: PS1 parsen",[11,188,189,190,193,194,196,197,200],{},"\"Dann parsen wir eben PS1 aus ",[15,191,192],{},"~/.bashrc","!\" Ungültig. PS1 wird aus Umgebungsvariablen, Funktionen, Git-Hooks, Virtualenv-Wrappern, Async-Status-Providern und zehn anderen Quellen zusammengebaut. Statisches Parsen der ",[15,195,100],{}," sieht nur einen Bruchteil davon. Und selbst wenn du die komplette Definition hättest — was bash am Terminal zeichnet, ist das ",[41,198,199],{},"Ergebnis der Expansion",", nicht die Quelltext-Form.",[124,202,204],{"id":203},"die-kern-einsicht","Die Kern-Einsicht",[11,206,207],{},"Bash sagt dir nicht \"ich bin fertig\". Der Prompt ist das einzige Signal, und er ist von Haus aus nicht zuverlässig lesbar.",[11,209,210,211,214,215,218],{},"Die richtige Antwort ist also nicht ",[41,212,213],{},"PS1 zu lesen"," — es ist ",[32,216,217],{},"PS1 zu überschreiben",".",[46,220,222],{"id":221},"der-trick-dein-eigener-marker","Der Trick: dein eigener Marker",[11,224,225],{},"Wenn bash dir keinen zuverlässigen \"fertig\"-Indikator gibt, gib ihr einen. Überschreibe PS1 mit einer Sentinel-Sequenz die du selbst definiert hast, und scanne den PTY-Output nach ihr. Wenn du den Marker siehst, weißt du dass bash den letzten Command beendet hat und auf den nächsten wartet.",[11,227,228,229],{},"Das klingt banal, ist aber der Moment wo die Architektur klickt: ",[32,230,231],{},"bash muss nicht verstehen dass sie in einem Wrapper läuft. Du änderst nur die Art wie sie \"fertig\" kommuniziert.",[11,233,234],{},"Die Idee in drei Schritten:",[236,237,238,241,244],"ol",{},[70,239,240],{},"Generiere einen Marker der nicht versehentlich in User-Output auftauchen kann.",[70,242,243],{},"Injizier ihn als PS1 beim Start der bash-Session.",[70,245,246],{},"Scanne den PTY-Stream nach dem Marker. Wenn du ihn siehst, war alles davor Command-Output, und bash ist bereit für die nächste Zeile.",[46,248,250],{"id":249},"die-details-die-tatsächlich-wichtig-sind","Die Details die tatsächlich wichtig sind",[124,252,254],{"id":253},"zufälliger-marker","Zufälliger Marker",[11,256,257,258,261,262,265,266,269],{},"Wenn du ",[15,259,260],{},"\"PROMPT>\""," als Marker nimmst und der User ",[15,263,264],{},"echo \"PROMPT>\""," eingibt, bist du verwirrt. Wenn du ",[15,267,268],{},"\"___END___\""," nimmst, gibt es vermutlich ein Logfile irgendwo auf der Welt das diese Zeichenkette enthält.",[11,271,272,273,276],{},"Lösung: ",[32,274,275],{},"16 Bytes Crypto-Random als Hex."," 32 Hex-Zeichen, 2^128 mögliche Werte. Kollisionsresistent in jeder realistischen Welt. In ape-shell sieht das so aus:",[54,278,282],{"className":279,"code":280,"language":281,"meta":60,"style":60},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { randomBytes } from 'node:crypto'\n\nthis.marker = randomBytes(16).toString('hex')\n","typescript",[15,283,284,317,324],{"__ignoreMap":60},[285,286,289,293,297,301,304,307,310,314],"span",{"class":287,"line":288},"line",1,[285,290,292],{"class":291},"s7zQu","import",[285,294,296],{"class":295},"sMK4o"," {",[285,298,300],{"class":299},"sTEyZ"," randomBytes",[285,302,303],{"class":295}," }",[285,305,306],{"class":291}," from",[285,308,309],{"class":295}," '",[285,311,313],{"class":312},"sfazB","node:crypto",[285,315,316],{"class":295},"'\n",[285,318,320],{"class":287,"line":319},2,[285,321,323],{"emptyLinePlaceholder":322},true,"\n",[285,325,327,330,333,336,339,342,346,349,351,354,356,359,362,364],{"class":287,"line":326},3,[285,328,329],{"class":295},"this.",[285,331,332],{"class":299},"marker ",[285,334,335],{"class":295},"=",[285,337,300],{"class":338},"s2Zo4",[285,340,341],{"class":299},"(",[285,343,345],{"class":344},"sbssI","16",[285,347,348],{"class":299},")",[285,350,218],{"class":295},[285,352,353],{"class":338},"toString",[285,355,341],{"class":299},[285,357,358],{"class":295},"'",[285,360,361],{"class":312},"hex",[285,363,358],{"class":295},[285,365,366],{"class":299},")\n",[11,368,369],{},"Mit etwas Struktur drumherum damit die Regex einen klaren Anker bekommt:",[54,371,374],{"className":372,"code":373,"language":59,"meta":60},[57],"__APES_\u003C32-hex-chars>__:\u003Cexit-code>:__END__\n",[15,375,373],{"__ignoreMap":60},[11,377,378,379,382,383,386,387,390],{},"Das Präfix ",[15,380,381],{},"__APES_"," macht es human-readable beim Debuggen. Das Suffix ",[15,384,385],{},":__END__"," gibt der Regex einen eindeutigen Terminator. Und der ",[15,388,389],{},"\u003Cexit-code>"," in der Mitte ist der Trick zu Bonus-Feature Nummer eins: du bekommst den Exit-Code des letzten Commands gleich mit dem Fertig-Signal, in einer einzigen Pattern-Match-Operation.",[124,392,394],{"id":393},"prompt_command-nicht-nur-ps1","PROMPT_COMMAND, nicht nur PS1",[11,396,397],{},"Das ist die Stelle wo fast alle ersten Implementationen einen Bug einbauen. Es reicht nicht, PS1 einmal beim Start zu setzen. Denn:",[11,399,400,406,407,410],{},[32,401,402,403,405],{},"Die ",[15,404,100],{}," des Users wird nach deinem Start gelesen."," Wenn dort ",[15,408,409],{},"PS1='...'"," steht — und das ist die Regel, nicht die Ausnahme — wird dein sorgfältig gesetzter Marker-PS1 überschrieben. Der User ist nicht schuld, aber dein Wrapper bricht.",[11,412,413,414,417,418,421,422,424],{},"Die Lösung ist eine Bash-Variable die die meisten Leute nicht kennen: ",[15,415,416],{},"PROMPT_COMMAND",". Das ist ein Shell-Kommando das bash ",[32,419,420],{},"vor jedem Prompt-Rendering"," ausführt. Wenn du dort PS1 neu setzt, überschreibst du alle ",[15,423,100],{},"-Konfigurationen des Users bevor der nächste Prompt gezeichnet wird:",[54,426,428],{"className":279,"code":427,"language":281,"meta":60,"style":60},"this.term = pty.spawn('bash', ['--login', '-i'], {\n  name: 'xterm-256color',\n  cols,\n  rows,\n  cwd: options.cwd ?? process.cwd(),\n  env: {\n    ...process.env,\n    // Force our marker PS1 on every prompt — survives .bashrc overrides.\n    PROMPT_COMMAND: `PS1='__APES_${this.marker}__:$?:__END__'`,\n    // Also set it initially so the very first prompt carries the marker.\n    PS1: `__APES_${this.marker}__:$?:__END__`,\n    PS2: '> ',\n    BASH_SILENCE_DEPRECATION_WARNING: '1',\n  },\n})\n",[15,429,430,486,505,512,520,552,562,578,585,618,624,651,668,685,691],{"__ignoreMap":60},[285,431,432,434,437,439,442,444,447,449,451,454,456,459,462,464,467,469,471,473,476,478,481,483],{"class":287,"line":288},[285,433,329],{"class":295},[285,435,436],{"class":299},"term ",[285,438,335],{"class":295},[285,440,441],{"class":299}," pty",[285,443,218],{"class":295},[285,445,446],{"class":338},"spawn",[285,448,341],{"class":299},[285,450,358],{"class":295},[285,452,453],{"class":312},"bash",[285,455,358],{"class":295},[285,457,458],{"class":295},",",[285,460,461],{"class":299}," [",[285,463,358],{"class":295},[285,465,466],{"class":312},"--login",[285,468,358],{"class":295},[285,470,458],{"class":295},[285,472,309],{"class":295},[285,474,475],{"class":312},"-i",[285,477,358],{"class":295},[285,479,480],{"class":299},"]",[285,482,458],{"class":295},[285,484,485],{"class":295}," {\n",[285,487,488,492,495,497,500,502],{"class":287,"line":319},[285,489,491],{"class":490},"swJcz","  name",[285,493,494],{"class":295},":",[285,496,309],{"class":295},[285,498,499],{"class":312},"xterm-256color",[285,501,358],{"class":295},[285,503,504],{"class":295},",\n",[285,506,507,510],{"class":287,"line":326},[285,508,509],{"class":299},"  cols",[285,511,504],{"class":295},[285,513,515,518],{"class":287,"line":514},4,[285,516,517],{"class":299},"  rows",[285,519,504],{"class":295},[285,521,523,526,528,531,533,536,539,542,544,547,550],{"class":287,"line":522},5,[285,524,525],{"class":490},"  cwd",[285,527,494],{"class":295},[285,529,530],{"class":299}," options",[285,532,218],{"class":295},[285,534,535],{"class":299},"cwd ",[285,537,538],{"class":295},"??",[285,540,541],{"class":299}," process",[285,543,218],{"class":295},[285,545,546],{"class":338},"cwd",[285,548,549],{"class":299},"()",[285,551,504],{"class":295},[285,553,555,558,560],{"class":287,"line":554},6,[285,556,557],{"class":490},"  env",[285,559,494],{"class":295},[285,561,485],{"class":295},[285,563,565,568,571,573,576],{"class":287,"line":564},7,[285,566,567],{"class":295},"    ...",[285,569,570],{"class":299},"process",[285,572,218],{"class":295},[285,574,575],{"class":299},"env",[285,577,504],{"class":295},[285,579,581],{"class":287,"line":580},8,[285,582,584],{"class":583},"sHwdD","    // Force our marker PS1 on every prompt — survives .bashrc overrides.\n",[285,586,588,591,593,596,599,602,604,607,610,613,616],{"class":287,"line":587},9,[285,589,590],{"class":490},"    PROMPT_COMMAND",[285,592,494],{"class":295},[285,594,595],{"class":295}," `",[285,597,598],{"class":312},"PS1='__APES_",[285,600,601],{"class":295},"${",[285,603,329],{"class":295},[285,605,606],{"class":299},"marker",[285,608,609],{"class":295},"}",[285,611,612],{"class":312},"__:$?:__END__'",[285,614,615],{"class":295},"`",[285,617,504],{"class":295},[285,619,621],{"class":287,"line":620},10,[285,622,623],{"class":583},"    // Also set it initially so the very first prompt carries the marker.\n",[285,625,627,630,632,634,636,638,640,642,644,647,649],{"class":287,"line":626},11,[285,628,629],{"class":490},"    PS1",[285,631,494],{"class":295},[285,633,595],{"class":295},[285,635,381],{"class":312},[285,637,601],{"class":295},[285,639,329],{"class":295},[285,641,606],{"class":299},[285,643,609],{"class":295},[285,645,646],{"class":312},"__:$?:__END__",[285,648,615],{"class":295},[285,650,504],{"class":295},[285,652,654,657,659,661,664,666],{"class":287,"line":653},12,[285,655,656],{"class":490},"    PS2",[285,658,494],{"class":295},[285,660,309],{"class":295},[285,662,663],{"class":312},"> ",[285,665,358],{"class":295},[285,667,504],{"class":295},[285,669,671,674,676,678,681,683],{"class":287,"line":670},13,[285,672,673],{"class":490},"    BASH_SILENCE_DEPRECATION_WARNING",[285,675,494],{"class":295},[285,677,309],{"class":295},[285,679,680],{"class":312},"1",[285,682,358],{"class":295},[285,684,504],{"class":295},[285,686,688],{"class":287,"line":687},14,[285,689,690],{"class":295},"  },\n",[285,692,694,696],{"class":287,"line":693},15,[285,695,609],{"class":295},[285,697,366],{"class":299},[11,699,700],{},"Drei Details die nicht offensichtlich sind:",[67,702,703,711,727],{},[70,704,705,710],{},[32,706,707],{},[15,708,709],{},"--login -i",": du willst dass die User-rcfiles gelesen werden, sonst fehlen Aliases, Functions und Environment die der User erwartet. Der Trade-off ist exakt der Grund warum du den PROMPT_COMMAND-Trick brauchst.",[70,712,713,718,719,722,723,726],{},[32,714,715],{},[15,716,717],{},"PS2='> '",": das ist der ",[41,720,721],{},"secondary prompt"," den bash nutzt wenn ein Command über mehrere Zeilen geht (unclosed quote, fortgeführte Pipe, ",[15,724,725],{},"if","-Block). Den setzt du auf etwas Einfaches damit du ihn beim Multi-Line-Handling erkennen kannst.",[70,728,729,734],{},[32,730,731],{},[15,732,733],{},"BASH_SILENCE_DEPRECATION_WARNING=1",": auf macOS meldet das System-bash bei jedem Start eine Deprecation-Warning auf stderr. Die verschmutzt deinen Output-Stream. Weg damit.",[124,736,738],{"id":737},"die-regex","Die Regex",[11,740,741],{},"Mit dem Marker im Output-Stream kannst du eine Regex bauen die ihn matcht und den Exit-Code extrahiert:",[54,743,745],{"className":279,"code":744,"language":281,"meta":60,"style":60},"this.markerRegex = new RegExp(\n  `__APES_${this.marker}__:(-?\\\\d+):__END__\\\\r?\\\\n?`,\n)\n",[15,746,747,765,803],{"__ignoreMap":60},[285,748,749,751,754,756,759,762],{"class":287,"line":288},[285,750,329],{"class":295},[285,752,753],{"class":299},"markerRegex ",[285,755,335],{"class":295},[285,757,758],{"class":295}," new",[285,760,761],{"class":338}," RegExp",[285,763,764],{"class":299},"(\n",[285,766,767,770,772,774,776,778,780,783,786,789,791,794,796,799,801],{"class":287,"line":319},[285,768,769],{"class":295},"  `",[285,771,381],{"class":312},[285,773,601],{"class":295},[285,775,329],{"class":295},[285,777,606],{"class":299},[285,779,609],{"class":295},[285,781,782],{"class":312},"__:(-?",[285,784,785],{"class":299},"\\\\",[285,787,788],{"class":312},"d+):__END__",[285,790,785],{"class":299},[285,792,793],{"class":312},"r?",[285,795,785],{"class":299},[285,797,798],{"class":312},"n?",[285,800,615],{"class":295},[285,802,504],{"class":295},[285,804,805],{"class":287,"line":326},[285,806,366],{"class":299},[11,808,809,810,813,814,817,818,821],{},"Das ",[15,811,812],{},"\\\\r?\\\\n?"," am Ende ist ein subtiler aber wichtiger Punkt: je nachdem wie bash den Prompt rendert (auf einer frischen Zeile oder direkt nach dem letzten Output), kann ein Newline folgen oder auch nicht. Die Regex toleriert beide Fälle. Die Gruppe ",[15,815,816],{},"(-?\\\\d+)"," fängt den Exit-Code ein, inklusive negativer Werte für Signale wie ",[15,819,820],{},"130"," oder bei unüblichen Konventionen.",[124,823,825],{"id":824},"der-output-parser","Der Output-Parser",[11,827,828],{},"Jeder PTY-Chunk der reinkommt wird an einen pending-Buffer angehängt und nach dem Marker gescannt. Wenn der Marker gefunden wird, ist alles davor das Output des gerade beendeten Commands:",[54,830,832],{"className":279,"code":831,"language":281,"meta":60,"style":60},"private handleData(chunk: string): void {\n  this.pending += chunk\n\n  for (;;) {\n    const match = this.pending.match(this.markerRegex)\n    if (!match || match.index === undefined) break\n\n    const before = this.pending.slice(0, match.index)\n    const exitCode = Number(match[1])\n\n    // Alles vor dem Marker ist Command-Output.\n    if (before.length > 0) {\n      this.currentLineBuffer += before\n      this.events.onOutput(before)\n    }\n\n    // Marker und alles davor aus dem Buffer werfen.\n    this.pending = this.pending.slice(match.index + match[0].length)\n\n    // Command ist fertig — Frame an den Consumer.\n    const frame = { output: this.currentLineBuffer, exitCode }\n    this.currentLineBuffer = ''\n    this.events.onLineDone(frame)\n  }\n\n  // Was jetzt noch in `pending` liegt, ist entweder partieller Output\n  // oder ein angefangener Marker der im nächsten Chunk weitergeht.\n}\n",[15,833,834,850,864,868,885,916,949,953,986,1010,1014,1019,1043,1056,1074,1079,1084,1090,1133,1138,1144,1172,1184,1203,1209,1214,1220,1226],{"__ignoreMap":60},[285,835,836,839,842,845,848],{"class":287,"line":288},[285,837,838],{"class":299},"private ",[285,840,841],{"class":338},"handleData",[285,843,844],{"class":299},"(chunk: string): ",[285,846,847],{"class":295},"void",[285,849,485],{"class":295},[285,851,852,855,858,861],{"class":287,"line":319},[285,853,854],{"class":295},"  this.",[285,856,857],{"class":299},"pending",[285,859,860],{"class":295}," +=",[285,862,863],{"class":299}," chunk\n",[285,865,866],{"class":287,"line":326},[285,867,323],{"emptyLinePlaceholder":322},[285,869,870,873,876,879,882],{"class":287,"line":514},[285,871,872],{"class":291},"  for",[285,874,875],{"class":490}," (",[285,877,878],{"class":295},";;",[285,880,881],{"class":490},") ",[285,883,884],{"class":295},"{\n",[285,886,887,891,894,897,900,902,904,907,909,911,914],{"class":287,"line":522},[285,888,890],{"class":889},"spNyl","    const",[285,892,893],{"class":299}," match",[285,895,896],{"class":295}," =",[285,898,899],{"class":295}," this.",[285,901,857],{"class":299},[285,903,218],{"class":295},[285,905,906],{"class":338},"match",[285,908,341],{"class":490},[285,910,329],{"class":295},[285,912,913],{"class":299},"markerRegex",[285,915,366],{"class":490},[285,917,918,921,923,926,928,931,933,935,938,941,944,946],{"class":287,"line":554},[285,919,920],{"class":291},"    if",[285,922,875],{"class":490},[285,924,925],{"class":295},"!",[285,927,906],{"class":299},[285,929,930],{"class":295}," ||",[285,932,893],{"class":299},[285,934,218],{"class":295},[285,936,937],{"class":299},"index",[285,939,940],{"class":295}," ===",[285,942,943],{"class":295}," undefined",[285,945,881],{"class":490},[285,947,948],{"class":291},"break\n",[285,950,951],{"class":287,"line":564},[285,952,323],{"emptyLinePlaceholder":322},[285,954,955,957,960,962,964,966,968,971,973,976,978,980,982,984],{"class":287,"line":580},[285,956,890],{"class":889},[285,958,959],{"class":299}," before",[285,961,896],{"class":295},[285,963,899],{"class":295},[285,965,857],{"class":299},[285,967,218],{"class":295},[285,969,970],{"class":338},"slice",[285,972,341],{"class":490},[285,974,975],{"class":344},"0",[285,977,458],{"class":295},[285,979,893],{"class":299},[285,981,218],{"class":295},[285,983,937],{"class":299},[285,985,366],{"class":490},[285,987,988,990,993,995,998,1000,1002,1005,1007],{"class":287,"line":587},[285,989,890],{"class":889},[285,991,992],{"class":299}," exitCode",[285,994,896],{"class":295},[285,996,997],{"class":338}," Number",[285,999,341],{"class":490},[285,1001,906],{"class":299},[285,1003,1004],{"class":490},"[",[285,1006,680],{"class":344},[285,1008,1009],{"class":490},"])\n",[285,1011,1012],{"class":287,"line":620},[285,1013,323],{"emptyLinePlaceholder":322},[285,1015,1016],{"class":287,"line":626},[285,1017,1018],{"class":583},"    // Alles vor dem Marker ist Command-Output.\n",[285,1020,1021,1023,1025,1028,1030,1033,1036,1039,1041],{"class":287,"line":653},[285,1022,920],{"class":291},[285,1024,875],{"class":490},[285,1026,1027],{"class":299},"before",[285,1029,218],{"class":295},[285,1031,1032],{"class":299},"length",[285,1034,1035],{"class":295}," >",[285,1037,1038],{"class":344}," 0",[285,1040,881],{"class":490},[285,1042,884],{"class":295},[285,1044,1045,1048,1051,1053],{"class":287,"line":670},[285,1046,1047],{"class":295},"      this.",[285,1049,1050],{"class":299},"currentLineBuffer",[285,1052,860],{"class":295},[285,1054,1055],{"class":299}," before\n",[285,1057,1058,1060,1063,1065,1068,1070,1072],{"class":287,"line":687},[285,1059,1047],{"class":295},[285,1061,1062],{"class":299},"events",[285,1064,218],{"class":295},[285,1066,1067],{"class":338},"onOutput",[285,1069,341],{"class":490},[285,1071,1027],{"class":299},[285,1073,366],{"class":490},[285,1075,1076],{"class":287,"line":693},[285,1077,1078],{"class":295},"    }\n",[285,1080,1082],{"class":287,"line":1081},16,[285,1083,323],{"emptyLinePlaceholder":322},[285,1085,1087],{"class":287,"line":1086},17,[285,1088,1089],{"class":583},"    // Marker und alles davor aus dem Buffer werfen.\n",[285,1091,1093,1096,1098,1100,1102,1104,1106,1108,1110,1112,1114,1116,1119,1121,1123,1125,1127,1129,1131],{"class":287,"line":1092},18,[285,1094,1095],{"class":295},"    this.",[285,1097,857],{"class":299},[285,1099,896],{"class":295},[285,1101,899],{"class":295},[285,1103,857],{"class":299},[285,1105,218],{"class":295},[285,1107,970],{"class":338},[285,1109,341],{"class":490},[285,1111,906],{"class":299},[285,1113,218],{"class":295},[285,1115,937],{"class":299},[285,1117,1118],{"class":295}," +",[285,1120,893],{"class":299},[285,1122,1004],{"class":490},[285,1124,975],{"class":344},[285,1126,480],{"class":490},[285,1128,218],{"class":295},[285,1130,1032],{"class":299},[285,1132,366],{"class":490},[285,1134,1136],{"class":287,"line":1135},19,[285,1137,323],{"emptyLinePlaceholder":322},[285,1139,1141],{"class":287,"line":1140},20,[285,1142,1143],{"class":583},"    // Command ist fertig — Frame an den Consumer.\n",[285,1145,1147,1149,1152,1154,1156,1159,1161,1163,1165,1167,1169],{"class":287,"line":1146},21,[285,1148,890],{"class":889},[285,1150,1151],{"class":299}," frame",[285,1153,896],{"class":295},[285,1155,296],{"class":295},[285,1157,1158],{"class":490}," output",[285,1160,494],{"class":295},[285,1162,899],{"class":295},[285,1164,1050],{"class":299},[285,1166,458],{"class":295},[285,1168,992],{"class":299},[285,1170,1171],{"class":295}," }\n",[285,1173,1175,1177,1179,1181],{"class":287,"line":1174},22,[285,1176,1095],{"class":295},[285,1178,1050],{"class":299},[285,1180,896],{"class":295},[285,1182,1183],{"class":295}," ''\n",[285,1185,1187,1189,1191,1193,1196,1198,1201],{"class":287,"line":1186},23,[285,1188,1095],{"class":295},[285,1190,1062],{"class":299},[285,1192,218],{"class":295},[285,1194,1195],{"class":338},"onLineDone",[285,1197,341],{"class":490},[285,1199,1200],{"class":299},"frame",[285,1202,366],{"class":490},[285,1204,1206],{"class":287,"line":1205},24,[285,1207,1208],{"class":295},"  }\n",[285,1210,1212],{"class":287,"line":1211},25,[285,1213,323],{"emptyLinePlaceholder":322},[285,1215,1217],{"class":287,"line":1216},26,[285,1218,1219],{"class":583},"  // Was jetzt noch in `pending` liegt, ist entweder partieller Output\n",[285,1221,1223],{"class":287,"line":1222},27,[285,1224,1225],{"class":583},"  // oder ein angefangener Marker der im nächsten Chunk weitergeht.\n",[285,1227,1229],{"class":287,"line":1228},28,[285,1230,1231],{"class":295},"}\n",[11,1233,1234,1235,1238,1239,1242,1243,1246],{},"Der subtile Punkt ist die Behandlung von ",[32,1236,1237],{},"partiellen Markern",". Ein PTY-Chunk kann mitten im Marker enden — bash hat den Anfang geschrieben, der Rest kommt mit dem nächsten ",[15,1240,1241],{},"data","-Event. Wenn du den pending-Buffer zwischenzeitlich weiterschiebst (etwa auf den letzten Newline trimst), zerstörst du den teilweisen Marker und die Erkennung schlägt fehl. Die Lösung: ",[32,1244,1245],{},"halte alle unmatched Bytes im pending-Buffer",", bis entweder der Marker komplett ankommt oder der Stream endet.",[124,1248,1250],{"id":1249},"die-bootstrap-phase","Die Bootstrap-Phase",[11,1252,1253,1254,1256,1257,1260,1261,1264],{},"Ein letztes Detail das einen ersten Durchlauf ruiniert: wenn du bash startest, wird zuerst ",[15,1255,192],{}," geladen. Das produziert oft Output — MOTDs, Shell-Init-Meldungen, ",[15,1258,1259],{},"nvm","-Status-Prints, alles Mögliche. Der erste Marker den du siehst, ist ",[32,1262,1263],{},"nicht"," das Ende eines User-Commands. Er ist das Ende des Startup-Prozesses.",[11,1266,1267],{},"Das heißt: zwei Phasen im State.",[67,1269,1270,1276],{},[70,1271,1272,1275],{},[32,1273,1274],{},"Phase 1 — Bootstrap:"," warte auf den ersten Marker. Verwirf alles was davor kam (Startup-Noise). Signalisiere dem Consumer \"bash is ready\".",[70,1277,1278,1281,1282,1284],{},[32,1279,1280],{},"Phase 2 — Normal:"," jeder weitere Marker ist das Ende eines User-Commands. Frames gehen via ",[15,1283,1195],{}," an den Consumer.",[11,1286,1287,1288,494],{},"Das ist in ape-shell ein einzelnes Boolean namens ",[15,1289,1290],{},"readyForFirstLine",[54,1292,1294],{"className":279,"code":1293,"language":281,"meta":60,"style":60},"if (!this.readyForFirstLine) {\n  // Bootstrap-Prompt: Startup-Noise wegwerfen, Ready signalisieren.\n  // onLineDone feuert hier bewusst NICHT — das wäre ein Fake-Frame\n  // aus Sicht des Consumers.\n  this.readyForFirstLine = true\n  this.currentLineBuffer = ''\n  const resolve = this.awaitingInitialPrompt\n  this.awaitingInitialPrompt = null\n  if (resolve) resolve()\n  continue\n}\n\n// Echtes Command-Ende: Frame übergeben.\nconst frame = { output: this.currentLineBuffer, exitCode }\nthis.currentLineBuffer = ''\nthis.events.onLineDone(frame)\n",[15,1295,1296,1310,1315,1320,1325,1337,1347,1362,1374,1391,1396,1400,1404,1409,1436,1447],{"__ignoreMap":60},[285,1297,1298,1300,1302,1305,1308],{"class":287,"line":288},[285,1299,725],{"class":291},[285,1301,875],{"class":299},[285,1303,1304],{"class":295},"!this.",[285,1306,1307],{"class":299},"readyForFirstLine) ",[285,1309,884],{"class":295},[285,1311,1312],{"class":287,"line":319},[285,1313,1314],{"class":583},"  // Bootstrap-Prompt: Startup-Noise wegwerfen, Ready signalisieren.\n",[285,1316,1317],{"class":287,"line":326},[285,1318,1319],{"class":583},"  // onLineDone feuert hier bewusst NICHT — das wäre ein Fake-Frame\n",[285,1321,1322],{"class":287,"line":514},[285,1323,1324],{"class":583},"  // aus Sicht des Consumers.\n",[285,1326,1327,1329,1331,1333],{"class":287,"line":522},[285,1328,854],{"class":295},[285,1330,1290],{"class":299},[285,1332,896],{"class":295},[285,1334,1336],{"class":1335},"sfNiH"," true\n",[285,1338,1339,1341,1343,1345],{"class":287,"line":554},[285,1340,854],{"class":295},[285,1342,1050],{"class":299},[285,1344,896],{"class":295},[285,1346,1183],{"class":295},[285,1348,1349,1352,1355,1357,1359],{"class":287,"line":564},[285,1350,1351],{"class":889},"  const",[285,1353,1354],{"class":299}," resolve",[285,1356,896],{"class":295},[285,1358,899],{"class":295},[285,1360,1361],{"class":299},"awaitingInitialPrompt\n",[285,1363,1364,1366,1369,1371],{"class":287,"line":580},[285,1365,854],{"class":295},[285,1367,1368],{"class":299},"awaitingInitialPrompt",[285,1370,896],{"class":295},[285,1372,1373],{"class":295}," null\n",[285,1375,1376,1379,1381,1384,1386,1388],{"class":287,"line":587},[285,1377,1378],{"class":291},"  if",[285,1380,875],{"class":490},[285,1382,1383],{"class":299},"resolve",[285,1385,881],{"class":490},[285,1387,1383],{"class":338},[285,1389,1390],{"class":490},"()\n",[285,1392,1393],{"class":287,"line":620},[285,1394,1395],{"class":291},"  continue\n",[285,1397,1398],{"class":287,"line":626},[285,1399,1231],{"class":295},[285,1401,1402],{"class":287,"line":653},[285,1403,323],{"emptyLinePlaceholder":322},[285,1405,1406],{"class":287,"line":670},[285,1407,1408],{"class":583},"// Echtes Command-Ende: Frame übergeben.\n",[285,1410,1411,1414,1417,1419,1421,1423,1425,1427,1429,1431,1434],{"class":287,"line":687},[285,1412,1413],{"class":889},"const",[285,1415,1416],{"class":299}," frame ",[285,1418,335],{"class":295},[285,1420,296],{"class":295},[285,1422,1158],{"class":490},[285,1424,494],{"class":295},[285,1426,899],{"class":295},[285,1428,1050],{"class":299},[285,1430,458],{"class":295},[285,1432,1433],{"class":299}," exitCode ",[285,1435,1231],{"class":295},[285,1437,1438,1440,1443,1445],{"class":287,"line":693},[285,1439,329],{"class":295},[285,1441,1442],{"class":299},"currentLineBuffer ",[285,1444,335],{"class":295},[285,1446,1183],{"class":295},[285,1448,1449,1451,1453,1455,1457],{"class":287,"line":1081},[285,1450,329],{"class":295},[285,1452,1062],{"class":299},[285,1454,218],{"class":295},[285,1456,1195],{"class":338},[285,1458,1459],{"class":299},"(frame)\n",[11,1461,1462],{},"Ohne diese Trennung bekommt der Consumer beim Start einen kaputten Frame mit allem rcfile-Noise als \"Output\" und einem willkürlichen Exit-Code. Das ist die Art Bug die du erst beim fünften User bemerkst, und dann ist die Ursache hart zu finden.",[46,1464,1466],{"id":1465},"eine-anwendung-ape-shells-ptybridge","Eine Anwendung: ape-shell's PtyBridge",[11,1468,1469,1470,1478,1479,1486],{},"Ich nutze dieses Pattern in ",[1471,1472,1476],"a",{"href":1473,"rel":1474},"https://github.com/openape-ai/openape/tree/main/packages/apes",[1475],"nofollow",[15,1477,17],{},", einem grant-secured Shell-Wrapper den ich für AI-Agent-Workflows baue. Die konkrete Implementation liegt in ",[1471,1480,1483],{"href":1481,"rel":1482},"https://github.com/openape-ai/openape/blob/main/packages/apes/src/shell/pty-bridge.ts",[1475],[15,1484,1485],{},"packages/apes/src/shell/pty-bridge.ts"," — etwas mehr als 200 Zeilen TypeScript die den kompletten Cycle abbilden: Spawn, Bootstrap, Line-Detection, Streaming-Output, Exit-Handling.",[11,1488,1489,1490,1493,1494,1497],{},"In ape-shell sitzt zwischen User-Eingabe und bash noch ein Grant-Check. Die PtyBridge selbst weiß davon nichts — sie kümmert sich nur um die saubere Abstraktion ",[41,1491,1492],{},"\"bash ist fertig mit dieser Zeile, hier ist der Output und der Exit-Code\"",". Der Grant-Layer darüber entscheidet ob eine Zeile überhaupt bei ",[15,1495,1496],{},"writeLine"," landet. Die Trennung der Concerns ist einer der Gründe warum sich das Pattern so natürlich anfühlt: die Marker-Detection ist ein universelles Problem, die Grant-Logik ist spezifisch.",[46,1499,1501],{"id":1500},"abschluss","Abschluss",[11,1503,1504],{},"Das Pattern ist nicht neu. Terminal-Emulatoren, REPL-Orchestratoren, Shell-Testing-Frameworks — sie alle lösen irgendeine Variante davon seit Jahrzehnten. expect, pexpect, bash-it's Test-Suite, IPython-Kernel, Jupyter-Frontends: alle haben irgendwo einen Marker-Trick. Aber er wird selten explizit beschrieben. Die meisten Entwickler die einen Shell-Wrapper schreiben stolpern selbst über die Lösung, manchmal erst nach der dritten naiven Implementation mit Timeouts und Newline-Heuristiken.",[11,1506,1507],{},"Wenn du irgendwann ein Tool baust das eine persistente Shell (oder ein anderes REPL mit Prompt-basiertem \"ready\"-Signal) kontrollieren will: das ist wahrscheinlich das Pattern das du suchst. Boring Infrastructure in its best sense — unsichtbar wenn es funktioniert, kritisch wenn es fehlt.",[1509,1510,1511],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":60,"searchDepth":319,"depth":319,"links":1513},[1514,1515,1522,1523,1530,1531],{"id":48,"depth":319,"text":49},{"id":121,"depth":319,"text":122,"children":1516},[1517,1518,1519,1520,1521],{"id":126,"depth":326,"text":127},{"id":147,"depth":326,"text":148},{"id":158,"depth":326,"text":159},{"id":185,"depth":326,"text":186},{"id":203,"depth":326,"text":204},{"id":221,"depth":319,"text":222},{"id":249,"depth":319,"text":250,"children":1524},[1525,1526,1527,1528,1529],{"id":253,"depth":326,"text":254},{"id":393,"depth":326,"text":394},{"id":737,"depth":326,"text":738},{"id":824,"depth":326,"text":825},{"id":1249,"depth":326,"text":1250},{"id":1465,"depth":319,"text":1466},{"id":1500,"depth":319,"text":1501},"2026-04-10","Wenn du einen Shell-Wrapper baust, der eine persistente bash über mehrere Commands hinweg kontrolliert, stolperst du über eine überraschend tiefe Frage: wann ist bash mit dem aktuellen Command durch? Ein kleiner Deep-Dive über Prompt-Marker, PROMPT_COMMAND und warum die Antwort nicht ist PS1 zu lesen — sondern PS1 zu überschreiben.",false,"md",null,{},"/blog/de/wie-weiss-ich-wann-bash-fertig-ist",{"title":5,"description":1533},"blog/de/wie-weiss-ich-wann-bash-fertig-ist",[1542,1543,1544,1545,1546],"Systems Programming","Shell","Bash","Technical Deep Dive","OpenApe","how-do-i-know-when-bash-is-done","zKA_O96D-Iev_ADvde1AiMzXVngL1lk4EQupMs4pzl4",{"de":1550,"en":1551},"/de/blog/wie-weiss-ich-wann-bash-fertig-ist","/en/blog/how-do-i-know-when-bash-is-done",1776970806601]