Как-то в очередной раз у меня возникла задача поменять пару строчек в нескольких файлах и я не стал использовать готовое решение, а задумал целую концепцию
Думаю, если накинуть сюда ещё парочку интересных задачек и решений, может получиться материал для неплохой статьи на Хабре для популяризации А2
Что-бы решить задачу замены строк в нескольких файлах надо либо качать какую-то утилиту, либо воспользоваться средствами, предоставляемыми командным интерпретатором системы.
Решение для командного интерпретатора системы мне показалось вообще страшным (что в нём вообще происходит, как понять):## Код:
```
@echo off
REM -- Prepare the Command Processor --
SETLOCAL ENABLEEXTENSIONS
SETLOCAL DISABLEDELAYEDEXPANSION

::BatchSubstitude - parses a File line by line and replaces a substring"
::syntax: BatchSubstitude.bat OldStr NewStr File
::          OldStr [in] - string to be replaced
::          NewStr [in] - string to replace with
::          File   [in] - file to be parsed
:$changed 20100115
:$source https://www.dostips.com
if "%~1"=="" findstr "^::" "%~f0"&GOTO:EOF
for /f "tokens=1,* delims=]" %%A in ('"type %3|find /n /v """') do (
    set "line=%%B"
    if defined line (
        call set "line=echo.%%line:%~1=%~2%%"
        for /f "delims=" %%X in ('"echo."%%line%%""') do %%~X
    ) ELSE echo.
)
```

А теперь Active Oberon way. Можно читателя на Хабре познакомить со всеми нюансами кода, например, обратить внимание на перегрузку операторов для удобного вывода сообщений в лог и на разбор параметров командной строки и надёжное закрытие файлов:## Код:
```
MODULE OberonConsoleUtils;

(** AUTHOR "Yaroslav Romanchenko (SAGE) http://sage.com.ua/ rapturize@gmail.com";
   PURPOSE "Different utilities usefull for console"; *)

IMPORT
   Commands, Options, Files, Strings, Streams;
   
CONST
   CRLF = ''0DX''0AX'';
   
TYPE
   String = Strings.String;
   Chars = ARRAY OF CHAR;
   
OPERATOR "+"(CONST a1, a2: Chars): String;
VAR str: String;
BEGIN
   NEW(str, Strings.Length(a1) + Strings.Length(a2) + 1);
   Strings.Concat(a1, a2, str^);
   RETURN str
END "+";

OPERATOR "+"(i: HUGEINT; CONST a2: Chars): String;
VAR
   a1: ARRAY 32 OF CHAR;
   str: String;
BEGIN
   Strings.IntToStr(i, a1);
   NEW(str, Strings.Length(a1) + Strings.Length(a2) + 1);
   Strings.Concat(a1, a2, str^);
   RETURN str
END "+";

VAR
   bSilent: BOOLEAN;

PROCEDURE Message(log: Streams.Writer; strMessage: String);
BEGIN
   IF ~bSilent THEN
      log.String(strMessage^)
   END
END Message;

PROCEDURE ReadFile(log: Streams.Writer; CONST aFileName: ARRAY OF CHAR; VAR iLength: LONGINT): String;
VAR
   file: Files.File;
   reader: Files.Reader;
   str: String;
BEGIN
   str := NIL;
   file := Files.Old(aFileName);
   IF file = NIL THEN
      Message(log, 'Bad file name: "' + aFileName + '"' + CRLF)
   ELSE
      NEW(str, file.Length() + 1);
      Files.OpenReader(reader, file, 0);
      reader.Bytes(str^, 0, LEN(str^), iLength)
   END;
   RETURN str
FINALLY
   IF file # NIL THEN
      file.Close()
   END
END ReadFile;

PROCEDURE WriteFile(log: Streams.Writer; CONST aFileName, aContent: ARRAY OF CHAR; iPos, iLength: LONGINT);
VAR
   file: Files.File;
   writer: Files.Writer;
BEGIN
   Message(log, 'Writing file "' + aFileName + '" ...' + CRLF);
   file := Files.New(aFileName);
   IF file = NIL THEN
      Message(log, 'Unable to create file: "' + aFileName + '"' + CRLF)
   ELSE
      Files.OpenWriter(writer, file, 0);
      writer.Bytes(aContent, iPos, iLength)
   END
FINALLY
   IF file # NIL THEN
      writer.Update();
      Files.Register(file);
      file.Close()
   END
END WriteFile;

PROCEDURE StringReplace(CONST aString, aOldPat, aNewPat: ARRAY OF CHAR; bIgnoreCase: BOOLEAN; VAR nReplaced: LONGINT): Strings.Buffer;
VAR
   length: LONGINT;
   from, pos, posLast: SIZE;
   buffer: Strings.Buffer;
   writer: Streams.Writer;
BEGIN
   length := LEN(aString);
   NEW(buffer, length + length DIV 16);
   writer := buffer.GetWriter();
   from := 0;
   posLast := -1;
   nReplaced := 0;
   REPEAT
      pos := Strings.GenericPos(aOldPat, from, aString, bIgnoreCase, FALSE);
      IF pos >= 0 THEN
         posLast := pos;
         INC(nReplaced);
         IF pos > 0 THEN
            writer.Bytes(aString, LONGINT(from), LONGINT(pos - from))
         END;
         writer.Bytes(aNewPat, 0, Strings.Length(aNewPat));
         from := pos + Strings.Length(aOldPat)
      ELSIF posLast >= 0 THEN (* Write tail *)
         writer.Bytes(aString, LONGINT(from), LONGINT(length - from))
      END
   UNTIL pos < 0;
   IF nReplaced = 0 THEN (* Nothing replaced, just copy *)
      writer.Bytes(aString, 0, LONGINT(length))
   END;
   RETURN buffer
END StringReplace;

(** Usage: ConsoleUtils.ReplaceStringInFile --old="old string" --new="new string" <Input file> [<Ouput file>] *)
PROCEDURE ReplaceStringInFile*(context: Commands.Context);
VAR
   bOptionsOk, bIgnoreCase: BOOLEAN;
   options: Options.Options;
   aOldPat, aNewPat: ARRAY 256 OF CHAR;
   aFileNameIn, aFileNameOut: Files.FileName;
   strContent: String;
   buffer: Strings.Buffer;
   nRead, nReplaced: LONGINT;
BEGIN
   NEW(options);
   options.Add("o", "old", Options.String);
   options.Add("n", "new", Options.String);
   options.Add("s", "silent", Options.Flag);
   options.Add("c", "case", Options.Flag); (* case sensitive *)
   bOptionsOk := options.Parse(context.arg, context.out) &
      options.GetString("old", aOldPat) & options.GetString("new", aNewPat);
   IF bOptionsOk THEN
      context.arg.SkipWhitespace();
      context.arg.String(aFileNameIn);
      bOptionsOk := aFileNameIn # ""
   END;
   
   bSilent := options.GetFlag("silent");
   bIgnoreCase := ~options.GetFlag("case");

   IF ~bOptionsOk THEN
      Message(context.out, CRLF + "Required parameters not provided. Usage:" + CRLF +
         'ConsoleUtils.ReplaceStringInFile --old="old string" --new="new string" <Input file> [<Ouput file>]' + CRLF)
   ELSE
      context.arg.SkipWhitespace();
      context.arg.String(aFileNameOut);
      strContent := ReadFile(context.out, aFileNameIn, nRead);
      IF strContent # NIL THEN
         Message(context.out, 'File "' + aFileNameIn + '" opened, ' + nRead + ' bytes read.' + CRLF);
         buffer := StringReplace(strContent^, aOldPat, aNewPat, bIgnoreCase, nReplaced);
         Message(context.out, nReplaced + ' occurences of pattern "' + aOldPat + '" found.' + CRLF);
         IF aFileNameOut # "" THEN
            WriteFile(context.out, aFileNameOut, buffer.GetString()^, 0, buffer.GetLength() - 1)
         ELSE
            WriteFile(context.out, aFileNameIn, buffer.GetString()^, 0, buffer.GetLength() - 1)
         END
      END
   END
END ReplaceStringInFile;

BEGIN
END OberonConsoleUtils.

System.Free OberonConsoleUtils ~
```
И вот, как это можно использовать. Например, если-бы вы разрабатывали что-то в среде CodeTyphon (как я в своё время разработал CGI движок своего сайта), вы бы могли столкнуться с ситуацией, когда автор CodeTyphon взял и поменял расширения для некоторых типов файлов в CodeTyphon... и, естественно, какие-то файлы на какие-то ссылаются. В общем, расширения поменять не проблема, но ссылки поправить уже придётся руками. Автор CodeTyphon не предусмотрел ни какой конверсии между версиями проектов.
И вот, теперь уже с применением А2 и моей небольшой утилиты можно так эту проблему решить:
## Код:
```
rem Convert project from CT v. 5.XX to CT v. 6.XX
rename *.lpi *.ctpr
rename *.lpr *.ppr
rename *.lfm *.frm
del oberon.txt
for %%G in (*.pas) do echo/OberonConsoleUtils.ReplaceStringInFile -o=*.lfm -n=*.frm %%G ~>>oberon.txt
for %%G in (*.ctpr) do echo/OberonConsoleUtils.ReplaceStringInFile -o=.lpr -n=.ppr %%G ~>>oberon.txt
echo/System.PowerDown ~>>oberon.txt
oberon.exe run oberon.txt
```
## Код:
```
rem Convert project from CT v. 6.XX to CT v. 5.XX
rename *.ctpr *.lpi 
rename *.ppr *.lpr
rename *.frm *.lfm 
del oberon.txt
for %%G in (*.pas) do echo/OberonConsoleUtils.ReplaceStringInFile -o=*.frm -n=*.lfm %%G ~>>oberon.txt
for %%G in (*.lpi) do echo/OberonConsoleUtils.ReplaceStringInFile -o=.ppr -n=.lpr %%G ~>>oberon.txt
echo/System.PowerDown ~>>oberon.txt
oberon.exe run oberon.txt
```
В итоге, для запуска такого батничка понадобится лишьoberon.exeи скомпилированныйOberonConsoleUtils.
В случае 32-х разрядной А2oberon.exeбудет занимать 952 Кб, аOberonConsoleUtils.GofW- 8,44 Кб
Неплохой такой контраст в мире гигабайтных сред разработки
Ведь можно утилиты и посерьёзнее написать, чем замена строк в файлах...
Что скажете, какие ещё идеи предложите?

PS. А в качестве среды разработки таких утилит можно предложитьWinA2Mini, собранную из последних исходников, которая в 7z архиве всего 9,12 Мб весит.