Как-то в очередной раз у меня возникла задача поменять пару строчек в нескольких файлах и я не стал использовать готовое решение
, а задумал целую концепцию
Думаю, если накинуть сюда ещё парочку интересных задачек и решений, может получиться материал для неплохой статьи на Хабре для популяризации А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-х разрядной А2
oberon.exe будет занимать 952 Кб, а
OberonConsoleUtils.GofW - 8,44 Кб
Неплохой такой контраст в мире гигабайтных сред разработки
Ведь можно утилиты и посерьёзнее написать, чем замена строк в файлах...
Что скажете, какие ещё идеи предложите?
PS. А в качестве среды разработки таких утилит можно предложить
WinA2Mini, собранную из последних исходников, которая в 7z архиве всего 9,12 Мб весит.