Converting my comment into an answer.
There are important differences between macros and environments:
environments are group surrounded, not so with macros.
a macro tokenizes its argument at the outset, so nothing that happens inside the macro can affect, for example, the catcodes of the tokens in the argument. In the environment, tokens from the input stream are absorbed on the fly, subject to changes that have transpired in the course of the environment.
environments allow trailing code to be executed, once the input stream is exhausted (and the trailing code is needed to close out the group opened by environment).
The MWE below demonstrates all three of these differences.
The tokcycle package allows one to cycle through the tokens of an argument or input stream and process them according to specified directives. The package provides both macro and pseudo-environment forms. By "pseudo-environment", I mean an environment requiring the use of \macro...\endmacro syntax, rather than the more familiar \begin{envname}...\end{envname} syntax.
In the MWE, I directly typeset (rather than store in a token register) the tokcycle-processed input. The processing is as follows: any token will be echoed to the output, except cat-7 ^ tokens, which will be output as an \fboxed string. When the process is complete, the value defined by \aftertokcycle is typeset, here being pre-set to an exclamation point !
I employ this processing using both macro and environment approaches to the following input: \chcat This is a ^ test, where \chcat is a macro that changes the catcode of ^ to a value of 12.
Item 1 is demonstrated by showing that, following the macro exit, the catcode of ^ remains at 12, whereas following the environment invocation, it returns (because of grouping) to its prior value of 7.
Item 2 is demonstrated by noting that only the ^ in the macro gets \fboxed. This is because, as part of a macro argument, the ^ is tokenized as catcode 7, regardless of what changes transpire in the course of executing the argument. In the environment alternative, the ^ is not boxed, because it has been tokenized only after the catcode of ^ has been changed to 12 in the course of executing the input stream.
Item 3 is demonstrated by the absence of the trailing ! in the environment version. Why? Because the environment form executes its own trailing code through the same macro employed by the \aftertokcycle invocation. Thus, the prior invocation of \aftertokcycle carries no sway over the environmental form, which uses its trailing code to redefine that variable. The macro form does not execute any trailing code, so a predefined \aftertokcycle still holds sway.
\documentclass{article}
\usepackage{tokcycle}
\def\chcat{\catcode`^=12}
\begin{document}
\Characterdirective{\tctestifcatnx^#1{\fbox{\string#1}}{#1}}
\Groupdirective{\processtoks{#1}}
\Macrodirective{#1}
\Spacedirective{#1}
\aftertokcycle{!}
Macro form:\\
\begingroup
\tokcyclexpress{\chcat This is a ^ test}
Caret catcode: \number\catcode`^
\endgroup
Environment form:\
\tokencyclexpress \chcat This is a ^ test\endtokencyclexpress
Caret catcode: \number\catcode`^
\end{document}

For the geek: The macro and environment forms of tokcyle used in this MWE both rely on the same underlying "raw" pseudoenvironment. The code to these interface forms may help to clarify why the macro acts like a macro and the pseudo-environment acts like an environment:
Macro Form (xpress interface):
% XPRESS-INTERFACE MACRO FORM
\long\def\tokcyclexpress#1{\tokcycrawxpress#1\endtokcycraw}
Pseudo-environment form (xpress interface):
% XPRESS-INTERFACE ENVIRONMENT FORM
\def\tokencyclexpress{\begingroup\let\endtokencyclexpress\endtokcycraw
\aftertokcycle{\the\cytoks\expandafter\endgroup\expandafter\tcenvscope
\expandafter{\the\cytoks}}\tokcycrawxpress}
fooyou are basically defining two macros\fooand\endfoo. I'm sure there is some other Q/A on the site.alignis somewhat different. – campa Oct 09 '20 at 14:46\beginand\enddo. – campa Oct 09 '20 at 15:25alignthat grab their body. – David Carlisle Oct 09 '20 at 17:40