Transaktional editieren
Die Edit-Befehle von greppy ändern genau das, was du benennst. Selektoren sind exakt, und ein Edit, dessen Vorbedingungen nicht gelten, scheitert, ohne etwas zu schreiben. Jeder angewendete Edit liefert eine Quittung, du musst die Datei zur Bestätigung also nicht wieder öffnen.
Die Beispiele laufen auf einer privaten Kopie von serde 1.0.229, die Prüfbeispiele auf einem kleinen Python-Projekt. Jeder Edit wurde danach mit greppy undo zurückgenommen.
Die Edit-Befehle
| Befehl | Tut |
|---|---|
greppy replace S [NEW] |
ersetzt eine Definition; --body ersetzt ihren Körper |
greppy replace-span H [NEW] |
ersetzt genau die Spanne, die ein Handle benennt; abgelehnt, wenn sich die Datei seit dem Druck des Handles geändert hat |
greppy replace-text F OLD [NEW] |
ersetzt OLD; abgelehnt, wenn es nicht genau einmal vorkommt (--expect N: genau N-mal; --regex: OLD ist ein regulärer Ausdruck) |
greppy replace-lines F A:B [NEW] |
ersetzt einen inklusiven, ab 1 gezählten Zeilenbereich |
greppy insert-lines F N [NEW] |
fügt nach Zeile N ein; Zeile 0 fügt oben ein |
greppy delete S |
entfernt eine Definition |
greppy delete-lines F A:B |
entfernt einen Zeilenbereich |
greppy write PATH [NEW] |
legt eine Datei an oder überschreibt sie |
greppy rename S NAME |
benennt eine Definition und jede vom Graphen aufgelöste Referenz um |
greppy patch [DIFF] |
wendet einen Unified Diff auf bestehende Dateien als eine Transaktion an |
greppy undo [ID] |
nimmt einen Edit zurück; ohne ID den letzten |
Fehlt NEW oder DIFF, liest greppy den Inhalt von stdin.
Ein typischer Edit
Lies die Definition mit Handle und ersetze genau diese Spanne:
$ greppy read RenameRule::or --handle
serde_derive/src/internals/case.rs:111-117 RenameRule::or
/// Returns the `RenameRule` if it is not `None`, `rule_b` otherwise.
pub fn or(self, rule_b: Self) -> Self {
match self {
None => rule_b,
_ => self,
}
}
handle: geh2:AorPGEstJxna8J-7pG_UieOOxt36q0czJg
$ greppy replace-span geh2:AorPGEstJxna8J-7pG_UieOOxt36q0czJg < or_if.rs
applied serde_derive/src/internals/case.rs:111-118 5eeeda
│
│ 108 }
│ 109 }
│ 110
│> 111 /// Returns the `RenameRule` if it is not `None`, `rule_b` otherwise.
│> 112 pub fn or(self, rule_b: Self) -> Self {
│> 113 if self == None {
│> 114 rule_b
│> 115 } else {
│> 116 self
│> 117 }
│> 118 }
│ 119 }
│ 120
│ 121 pub struct ParseError<'a> {
Hat sich die Datei seither geändert, schreibt dasselbe Handle nichts mehr, Exit-Code 12:
$ greppy replace-span geh2:AorPGEstJxna8J-7pG_UieOOxt36q0czJg < or_if.rs
serde_derive/src/internals/case.rs changed since that handle was taken
Eine ganze Definition ersetzt du über ihren Namen. --body ersetzt den Körper samt Klammern, --dry-run zeigt vorher, was sich ändern würde:
$ greppy replace RenameRule::or --body --dry-run < or_body.rs
would apply serde_derive/src/internals/case.rs:112-118
Text- und Zeilen-Edits arbeiten auf Dateien:
greppy replace-text serde_derive/src/internals/case.rs 'expected one of ' 'expected one of: '
greppy replace-lines serde_derive/src/internals/case.rs 113:115 < or_lines.rs
greppy insert-lines serde_derive/src/internals/ctxt.rs 0 < header.rs
$ greppy replace-text serde_derive/src/internals/case.rs 'expected one of ' 'expected one of: '
applied serde_derive/src/internals/case.rs:128 2f9ff2
│
│ 125 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
│ 126 f.write_str("unknown rename rule `rename_all = ")?;
│ 127 Debug::fmt(self.unknown, f)?;
│> 128 f.write_str("`, expected one of: ")?;
│ 129 for (i, (name, _rule)) in RENAME_RULES.iter().enumerate() {
│ 130 if i > 0 {
│ 131 f.write_str(", ")?;
$ greppy insert-lines serde_derive/src/internals/ctxt.rs 0 < header.rs
applied serde_derive/src/internals/ctxt.rs:1-2 c7ade8
│
│> 1 // Error collection shared by the derive macros.
│> 2
│ 3 use quote::ToTokens;
│ 4 use std::cell::RefCell;
│ 5 use std::fmt::Display;
Mit --regex fügt $1 oder ${name} in NEW eine Gruppe ein, $$ ein wörtliches $. Unbekannte Gruppen lehnt greppy ab, bevor es etwas schreibt:
$ greppy replace-text serde_derive/src/internals/case.rs --regex 'children to "([^"]+)" style' 'children to `$1` style' --expect 8
applied serde_derive/src/internals/case.rs:12,14,16,19,21,24,27,29 418ff3
Aus /// Rename direct children to "lowercase" style. wird dabei /// Rename direct children to `lowercase` style., in allen acht Doc-Kommentaren des Enums RenameRule.
Quittungen und Ablehnungen
Eine Quittung nennt jede berührte Stelle und die Edit-ID. Darunter steht die gelandete Spanne mit bis zu drei Zeilen Kontext, frisch von der Platte gelesen, die gelandeten Zeilen mit > markiert. Ein Edit an mehreren Stellen listet sie alle, etwa applied serde_derive/src/internals/case.rs:56,81 ebb5f8 für --expect 2. patch und rename drucken eine solche Zeile pro Datei, alle mit derselben Edit-ID.
Eine Ablehnung nennt ihren Grund mit Belegen und schreibt nichts, Exit-Code 13:
$ greppy replace-text serde_derive/src/internals/case.rs 'returning the version expected in the source' 'as written in the source'
OLD occurs 2 times — nothing written
serde_derive/src/internals/case.rs:56:51: /// Apply a renaming rule to an enum variant, returning the version expected in the source.
serde_derive/src/internals/case.rs:81:50: /// Apply a renaming rule to a struct field, returning the version expected in the source.
Unterscheidet sich OLD von einer Stelle in der Datei allein im Whitespace, nennt die Ablehnung diese Stelle mit Zeilenbereich, exaktem Text und der Alternative replace-lines, Exit-Code 13:
$ greppy replace-text serde_derive/src/internals/case.rs "$(cat old_ws.txt)" 'x'
OLD occurs 0 times — nothing written
nearest match differs only in whitespace at serde_derive/src/internals/case.rs:130-131; copy these lines exactly as OLD, or use: greppy replace-lines serde_derive/src/internals/case.rs 130:131 [NEW]
if i > 0 {
f.write_str(", ")?;
Vor dem Schreiben prüft greppy die Syntax des Ergebnisses. Ein Edit, der neue Syntaxfehler einführt, wird abgelehnt, auch ohne --verify, Exit-Code 13:
$ greppy replace-text units.py 'return high' 'return high)'
refused: syntax validation failed in proposed units.py:9:21 (Python suite requires a statement; --body replacements must include indentation (for example, four spaces before return); use pass for an empty body; column is a byte offset); errors 0 -> 2, missing nodes 0 -> 0 — nothing written. Location refers to the proposed result, not the unchanged file
Ein Edit, dessen Selektor nichts trifft oder dessen Symbol mehrdeutig ist, endet mit einem Exit-Code ungleich 0 und schreibt nichts.
Vorschau und Prüfung
| Flag | Wirkung |
|---|---|
--dry-run |
meldet, was sich ändern würde, etwa would apply serde_derive/src/internals/case.rs:112-118, und ändert nichts |
--verify |
führt einen begrenzten lokalen Checker für die Sprache der berührten Datei aus und meldet seine Diagnosen |
--json |
die Quittung als Daten, mit Hashes vor und nach dem Edit und einem Handle auf die neue Spanne |
--verify druckt den gewählten Befehl und seinen Live-Zustand, dann das Ergebnis. Projekt-Tests laufen, wenn du sie mit GREPPY_VERIFY_TEST_COMMAND auswählst:
$ greppy replace clamp --verify < clamp_ok.py
verify: running Python syntax check — python3 -m py_compile units.py (timeout 60s)
verify: passed — Python syntax check
verify: tests not run — select affected tests with GREPPY_VERIFY_TEST_COMMAND or run them through `greppy bash-smart` before finishing
applied units.py:4-6 2e04c7
│
│ 1 """Small unit conversion helpers."""
│ 2
│ 3
│> 4 def clamp(value, low, high):
│> 5 """Restrict a value to the closed range [low, high]."""
│> 6 return max(low, min(value, high))
│ 7
│ 8
│ 9 def celsius_to_fahrenheit(celsius):
verify: passed — Python syntax check
verify: tests not run — select affected tests with GREPPY_VERIFY_TEST_COMMAND or run them through `greppy bash-smart` before finishing
Mit GREPPY_VERIFY_TEST_COMMAND läuft der Testbefehl über greppy bash-smart in derselben begrenzten Prüfung. Ein gescheiterter Check nimmt den Edit nicht zurück: Der Edit bleibt angewendet, die Quittung meldet verify: failed, und der Befehl endet mit Exit-Code 17:
$ GREPPY_VERIFY_TEST_COMMAND='python3 -m unittest discover -s tests -t .' greppy replace-text units.py ' return high' ' return high - 1' --verify
verify: running Python syntax check — python3 -m py_compile units.py (timeout 60s)
verify: passed — Python syntax check
…
verify: failed (exit 1) — selected project tests
applied units.py:9 4a15f0
│
│ 6 if value < low:
│ 7 return low
│ 8 if value > high:
│> 9 return high - 1
│ 10 return value
…
verify: failed (exit 1) — selected project tests
AssertionError: 2 != 3
FAILED — exit 1: 4 errors, 0 warnings
verify: edit remains applied; repair it or use `greppy undo` to restore the previous edit
Nimm den Edit mit greppy undo zurück oder repariere ihn mit dem nächsten Edit. Der Checker richtet sich nach der Sprache der berührten Datei, für units.py ist das python3 -m py_compile.
patch
greppy patch --dry-run < or_and_doc.diff
greppy patch --verify < or_and_doc.diff
patch nimmt einen Unified Diff und landet ihn als Ganzes. Hunks verankern sich an ihren Kontextzeilen, die Zeilennummern in @@ sind Hinweise. Im folgenden Diff steht der zweite Hunk in Wahrheit bei Zeile 37, nicht bei 20:
--- a/serde_derive/src/internals/case.rs
+++ b/serde_derive/src/internals/case.rs
@@ -110,9 +110,10 @@
/// Returns the `RenameRule` if it is not `None`, `rule_b` otherwise.
pub fn or(self, rule_b: Self) -> Self {
- match self {
- None => rule_b,
- _ => self,
+ if self == None {
+ rule_b
+ } else {
+ self
}
}
}
--- a/serde_derive/src/internals/ctxt.rs
+++ b/serde_derive/src/internals/ctxt.rs
@@ -20,7 +20,7 @@
.push(syn::Error::new_spanned(obj.into_token_stream(), msg));
}
- /// Add one of Syn's parse errors.
+ /// Record one of Syn's parse errors.
pub fn syn_error(&self, err: syn::Error) {
self.errors.borrow_mut().as_mut().unwrap().push(err);
}
$ greppy patch --dry-run < or_and_doc.diff
would apply serde_derive/src/internals/case.rs:110-119
would apply serde_derive/src/internals/ctxt.rs:37-43
$ greppy patch < or_and_doc.diff
applied serde_derive/src/internals/case.rs:110-119 09b3f2
applied serde_derive/src/internals/ctxt.rs:37-43 09b3f2
$ greppy undo
reversed 09b3f2 2 files
Jede Datei im Diff ändert sich gemeinsam mit den anderen, oder keine. Passt der Kontext eines Hunks nicht, lehnt greppy den ganzen Patch ab, auch wenn andere Hunks passen würden, Exit-Code 13:
$ greppy patch < or_and_doc_bad_context.diff
serde_derive/src/internals/ctxt.rs: input hunk 2 at patch line 19: hunk context did not match (the @@ line 37 is advisory). Removed and context lines must match complete source lines, not substrings; regenerate with `git diff --no-color -- PATH`, or use `greppy replace-text` for an intentional substring replacement — nothing written
Fasse zusammengehörige Änderungen in einem patch zusammen statt in einer Reihe einzelner Edits. Ein greppy undo nimmt den ganzen Patch zurück.
patch ändert bestehende Dateien. Eine neue Datei legst du mit greppy write PATH an, den Inhalt auf stdin. Das ist eine eigene Transaktion, getrennt von den Edits eines Patches, Exit-Code 20:
$ greppy patch < newfile.diff
new_mod.py: patch only edits existing files; to create a file, use `greppy write PATH` with its content on stdin. This is a separate transaction, not atomic with edits in this patch — nothing written
$ printf 'X = 1\n' | greppy write new_mod.py
applied new_mod.py:1 5a79a7
│
│> 1 X = 1
rename
greppy rename visit_content_map visit_content_entries --dry-run
greppy rename visit_content_map visit_content_entries
rename ändert die Definition und jede Referenz, die der Graph auflöst. Für visit_content_map sind das genau die drei Aufrufer, die who-calls nennt, plus die Definition:
$ greppy rename visit_content_map visit_content_entries --dry-run
would apply serde/src/private/de.rs:1103,1152,1399,1415
$ greppy rename visit_content_map visit_content_entries
applied serde/src/private/de.rs:1103,1152,1399,1415 bf5b9c
$ git diff -U0
…
@@ -1103 +1103 @@ mod content {
- fn visit_content_map<'de, V, E>(
+ fn visit_content_entries<'de, V, E>(
@@ -1152 +1152 @@ mod content {
- Content::Map(v) => visit_content_map(v, visitor),
+ Content::Map(v) => visit_content_entries(v, visitor),
…
$ greppy rename wrap_in_const wrap_in_dummy_const
applied serde_derive/src/de.rs:78 e57388
applied serde_derive/src/dummy.rs:4 e57388
applied serde_derive/src/ser.rs:60 e57388
Findet greppy den alten Namen als Bezeichner an Stellen, an denen der Graph keine Referenz aufgelöst hat, lehnt es die Umbenennung ab und schreibt nichts, Exit-Code 12:
$ greppy rename Ctxt::error_spanned_by error_at
graph reference scope serde_derive/src/internals/attr.rs contains 5 live `error_spanned_by` identifiers for `Ctxt::error_spanned_by`; select a narrower symbol or refresh the index — nothing written
Text, den der Graph nicht als Referenz modelliert, etwa Kommentare, Strings oder Lookups über Reflection, benennt rename nicht um und meldet ihn nicht. Suche nach einer Umbenennung mit greppy search-pattern ALT --fixed nach dem alten Namen. Mehr dazu liest du unter Grenzen.
undo
greppy undo # den letzten Edit zurücknehmen
greppy undo ID # einen benannten Edit zurücknehmen
$ greppy undo
reversed 5eeeda serde_derive/src/internals/case.rs
undo ID nimmt einen älteren Edit zurück, solange seine Dateien noch so aussehen, wie dieser Edit sie hinterlassen hat. Spätere Edits an anderen Dateien stören nicht. Hat ein späterer Edit dieselbe Datei geändert, lehnt greppy ab, Exit-Code 12:
$ greppy undo ebb5f8
serde_derive/src/internals/case.rs no longer looks the way that edit left it
Nimm dann zuerst die späteren Edits zurück: greppy undo ohne ID geht die Kette rückwärts, bis nothing to undo in this workspace (Exit-Code 10) kommt. Nutze undo, um einen veröffentlichten Edit zurückzunehmen, statt den alten Text von Hand zu rekonstruieren. undo nach write entfernt die angelegte Datei wieder.
Exit-Codes
Die Beispiele dieser Seite liefern diese Exit-Codes:
| Code | Situation |
|---|---|
| 0 | Edit angewendet, Vorschau oder Undo erfolgreich |
| 10 | nichts mehr zum Zurücknehmen |
| 12 | Handle veraltet, undo auf eine später geänderte Datei, rename mit unaufgelösten Bezeichnern |
| 13 | OLD kommt nicht genau so oft vor wie verlangt, Syntaxfehler im Ergebnis, Hunk-Kontext passt nicht |
| 17 | Edit angewendet, --verify gescheitert |
| 20 | patch mit einer neuen Datei |
Agents und Edits
Der Agent-Prompt sagt Agents, einen präzisen Edit vorzuziehen oder für zusammengehörige Änderungen ein greppy patch --verify, und berührten Quelltext nach einem erfolgreich geprüften Edit nicht noch einmal zu öffnen. Nachfassen ist angesagt, wenn das Ergebnis eine Mehrdeutigkeit, unvollständige Belege oder eine gescheiterte Prüfung meldet. Siehe Agent einrichten.