syntax.bash.pp 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. {
  2. This file is part of the Free Component Library (FCL)
  3. Copyright (c) 2025 by Michael Van Canneyt
  4. Bash syntax highlighter
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. {$MODE objfpc}
  12. {$H+}
  13. unit syntax.bash;
  14. interface
  15. uses
  16. {$IFDEF FPC_DOTTEDUNITS}System.{$ENDIF}Types, syntax.highlighter;
  17. type
  18. { TBashSyntaxHighlighter }
  19. TBashSyntaxHighlighter = class(TSyntaxHighlighter)
  20. private
  21. FSource: string;
  22. FPos: integer;
  23. protected
  24. procedure ProcessSingleQuoteString(var endPos: integer);
  25. procedure ProcessDoubleQuoteString(var endPos: integer);
  26. procedure ProcessBackquoteString(var endPos: integer);
  27. procedure ProcessVariable(var endPos: integer);
  28. procedure ProcessComment(var endPos: integer);
  29. function CheckForKeyword(var endPos: integer): boolean;
  30. procedure ProcessNumber(var endPos: integer);
  31. procedure ProcessOperator(var endPos: integer);
  32. function IsWordChar(ch: char): boolean;
  33. class function GetLanguages: TStringDynArray; override;
  34. procedure checkcategory; virtual;
  35. public
  36. constructor create; override;
  37. class var CategoryBash : Integer;
  38. function Execute(const Source: string): TSyntaxTokenArray; override;
  39. end;
  40. const
  41. MaxKeywordLength = 15;
  42. MaxKeyword = 39;
  43. BashKeywordTable: array[0..MaxKeyword] of string = (
  44. 'case', 'do', 'done', 'elif', 'else', 'esac', 'fi', 'for', 'function', 'if',
  45. 'in', 'select', 'then', 'until', 'while', 'break', 'continue', 'return', 'exit', 'declare',
  46. 'local', 'readonly', 'export', 'alias', 'unalias', 'type', 'which', 'command', 'builtin', 'enable',
  47. 'help', 'let', 'eval', 'exec', 'source', 'trap', 'umask', 'ulimit', 'set', 'shift'
  48. );
  49. function DoBashHighlighting(const Source: string): TSyntaxTokenArray;
  50. implementation
  51. { TBashSyntaxHighlighter }
  52. procedure TBashSyntaxHighlighter.ProcessSingleQuoteString(var endPos: integer);
  53. var
  54. startPos: integer;
  55. begin
  56. startPos := FPos;
  57. Inc(FPos); // Skip opening quote
  58. while (FPos <= Length(FSource)) and (FSource[FPos] <> '''') do
  59. Inc(FPos);
  60. if (FPos <= Length(FSource)) and (FSource[FPos] = '''') then
  61. Inc(FPos); // Include closing quote
  62. endPos := FPos - 1;
  63. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shStrings);
  64. end;
  65. procedure TBashSyntaxHighlighter.ProcessDoubleQuoteString(var endPos: integer);
  66. var
  67. startPos: integer;
  68. begin
  69. startPos := FPos;
  70. Inc(FPos); // Skip opening quote
  71. while FPos <= Length(FSource) do
  72. begin
  73. if FSource[FPos] = '"' then
  74. begin
  75. Inc(FPos);
  76. break;
  77. end
  78. else if FSource[FPos] = '\' then
  79. begin
  80. if FPos < Length(FSource) then
  81. Inc(FPos); // Skip escaped character
  82. end;
  83. Inc(FPos);
  84. end;
  85. endPos := FPos - 1;
  86. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shStrings);
  87. end;
  88. procedure TBashSyntaxHighlighter.ProcessBackquoteString(var endPos: integer);
  89. var
  90. startPos: integer;
  91. begin
  92. startPos := FPos;
  93. Inc(FPos); // Skip opening backquote
  94. while (FPos <= Length(FSource)) and (FSource[FPos] <> '`') do
  95. begin
  96. if FSource[FPos] = '\' then
  97. begin
  98. if FPos < Length(FSource) then
  99. Inc(FPos); // Skip escaped character
  100. end;
  101. Inc(FPos);
  102. end;
  103. if (FPos <= Length(FSource)) and (FSource[FPos] = '`') then
  104. Inc(FPos); // Include closing backquote
  105. endPos := FPos - 1;
  106. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shInterpolation);
  107. end;
  108. procedure TBashSyntaxHighlighter.ProcessVariable(var endPos: integer);
  109. var
  110. startPos: integer;
  111. begin
  112. startPos := FPos;
  113. Inc(FPos); // Skip $
  114. // Handle special variables like $?, $$, $!, etc.
  115. if (FPos <= Length(FSource)) and (FSource[FPos] in ['?', '$', '!', '#', '*', '@', '-', '0'..'9']) then
  116. begin
  117. Inc(FPos);
  118. end
  119. // Handle ${variable} syntax
  120. else if (FPos <= Length(FSource)) and (FSource[FPos] = '{') then
  121. begin
  122. Inc(FPos); // Skip {
  123. while (FPos <= Length(FSource)) and (FSource[FPos] <> '}') do
  124. Inc(FPos);
  125. if (FPos <= Length(FSource)) and (FSource[FPos] = '}') then
  126. Inc(FPos); // Include }
  127. end
  128. // Handle regular variable names
  129. else
  130. begin
  131. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  132. Inc(FPos);
  133. end;
  134. endPos := FPos - 1;
  135. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault); // Variables as default for now
  136. end;
  137. procedure TBashSyntaxHighlighter.ProcessComment(var endPos: integer);
  138. var
  139. startPos: integer;
  140. begin
  141. startPos := FPos;
  142. while (FPos <= Length(FSource)) and (FSource[FPos] <> #10) and (FSource[FPos] <> #13) do
  143. Inc(FPos);
  144. endPos := FPos - 1;
  145. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shComment);
  146. end;
  147. function TBashSyntaxHighlighter.CheckForKeyword(var endPos: integer): boolean;
  148. var
  149. i, j: integer;
  150. keyword: string;
  151. begin
  152. i := 0;
  153. while (FPos+i<=Length(FSource)) and (i < MaxKeywordLength) and IsWordChar(FSource[FPos + i]) do
  154. Inc(i);
  155. keyword:=Copy(FSource,FPos,i);
  156. Result := False;
  157. for j := 0 to MaxKeyword do
  158. if BashKeywordTable[j] = keyword then
  159. begin
  160. Result := True;
  161. break;
  162. end;
  163. if not Result then
  164. Exit;
  165. Inc(FPos, i);
  166. endPos := FPos - 1;
  167. AddToken(keyword, shKeyword);
  168. end;
  169. procedure TBashSyntaxHighlighter.ProcessNumber(var endPos: integer);
  170. var
  171. startPos: integer;
  172. begin
  173. startPos := FPos;
  174. while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
  175. Inc(FPos);
  176. // Handle decimal point
  177. if (FPos <= Length(FSource)) and (FSource[FPos] = '.') then
  178. begin
  179. Inc(FPos);
  180. while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
  181. Inc(FPos);
  182. end;
  183. endPos := FPos - 1;
  184. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shNumbers);
  185. end;
  186. procedure TBashSyntaxHighlighter.ProcessOperator(var endPos: integer);
  187. var
  188. startPos: integer;
  189. ch: char;
  190. begin
  191. startPos := FPos;
  192. ch := FSource[FPos];
  193. // Handle multi-character operators
  194. case ch of
  195. '=':
  196. begin
  197. Inc(FPos);
  198. if (FPos <= Length(FSource)) and (FSource[FPos] = '=') then
  199. Inc(FPos);
  200. end;
  201. '!':
  202. begin
  203. Inc(FPos);
  204. if (FPos <= Length(FSource)) and (FSource[FPos] = '=') then
  205. Inc(FPos);
  206. end;
  207. '<':
  208. begin
  209. Inc(FPos);
  210. if (FPos <= Length(FSource)) and (FSource[FPos] in ['=', '<']) then
  211. Inc(FPos);
  212. end;
  213. '>':
  214. begin
  215. Inc(FPos);
  216. if (FPos <= Length(FSource)) and (FSource[FPos] in ['=', '>']) then Inc(FPos);
  217. end;
  218. '&':
  219. begin
  220. Inc(FPos);
  221. if (FPos <= Length(FSource)) and (FSource[FPos] = '&') then Inc(FPos);
  222. end;
  223. '|':
  224. begin
  225. Inc(FPos);
  226. if (FPos <= Length(FSource)) and (FSource[FPos] = '|') then Inc(FPos);
  227. end;
  228. else
  229. Inc(FPos);
  230. end;
  231. endPos := FPos - 1;
  232. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shOperator);
  233. end;
  234. function TBashSyntaxHighlighter.IsWordChar(ch: char): boolean;
  235. begin
  236. Result := ch in ['a'..'z', 'A'..'Z', '0'..'9', '_'];
  237. end;
  238. class function TBashSyntaxHighlighter.GetLanguages: TStringDynArray;
  239. begin
  240. Result:=['bash','sh','zsh']
  241. end;
  242. procedure TBashSyntaxHighlighter.checkcategory;
  243. begin
  244. if CategoryBash=0 then
  245. CategoryBash:=RegisterCategory('bash');
  246. end;
  247. constructor TBashSyntaxHighlighter.create;
  248. begin
  249. inherited create;
  250. CheckCategory;
  251. DefaultCategory:=CategoryBash;
  252. end;
  253. function TBashSyntaxHighlighter.Execute(const Source: string): TSyntaxTokenArray;
  254. var
  255. lLen,startPos,endPos: integer;
  256. ch: char;
  257. begin
  258. Result:=Nil;
  259. if Length(Source) = 0 then
  260. Exit;
  261. FSource := Source;
  262. lLen:=Length(FSource);
  263. FPos := 1;
  264. endpos:=0;
  265. while FPos <= lLen do
  266. begin
  267. ch := FSource[FPos];
  268. case ch of
  269. '#':
  270. ProcessComment(endPos);
  271. '''':
  272. ProcessSingleQuoteString(endPos);
  273. '"':
  274. ProcessDoubleQuoteString(endPos);
  275. '`':
  276. ProcessBackquoteString(endPos);
  277. '$':
  278. ProcessVariable(endPos);
  279. '0'..'9':
  280. ProcessNumber(endPos);
  281. 'a'..'z', 'A'..'Z', '_':
  282. begin
  283. if not CheckForKeyword(endPos) then
  284. begin
  285. startPos := FPos;
  286. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  287. Inc(FPos);
  288. endPos := FPos - 1;
  289. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault);
  290. end;
  291. end;
  292. '=', '!', '<', '>', '&', '|', '+', '-', '*', '/', '%', '^': ProcessOperator(endPos);
  293. ';', '(', ')', '[', ']', '{', '}', ',':
  294. begin
  295. AddToken(ch, shSymbol);
  296. endPos := FPos;
  297. Inc(FPos);
  298. end;
  299. ' ', #9, #10, #13:
  300. begin
  301. startPos := FPos;
  302. while (FPos <= Length(FSource)) and (FSource[FPos] in [' ', #9, #10, #13]) do
  303. Inc(FPos);
  304. endPos := FPos - 1;
  305. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault);
  306. end;
  307. else
  308. AddToken(ch, shInvalid);
  309. endPos := FPos;
  310. Inc(FPos);
  311. end;
  312. if FPos = endPos then
  313. Inc(FPos);
  314. end;
  315. Result := FTokens.GetTokens;
  316. end;
  317. function DoBashHighlighting(const Source: string): TSyntaxTokenArray;
  318. var
  319. highlighter: TBashSyntaxHighlighter;
  320. begin
  321. highlighter := TBashSyntaxHighlighter.Create;
  322. try
  323. Result := highlighter.Execute(Source);
  324. finally
  325. highlighter.Free;
  326. end;
  327. end;
  328. initialization
  329. TBashSyntaxHighlighter.Register;
  330. end.