Typesetting hashtags with natural syntax

enter image description here

\documentclass{article}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\begin{document}

\def\hashtag{%
\textcolor{cyan}{\#}%
\begingroup
\color{red}%
\xhashtag}

\def\xhashtag{\futurelet\tmp\xxhashtag}

\def\xyhashtag#1{\xhashtag}

\def\xxhashtag{%
\ifcat a\noexpand\tmp 
 \tmp\expandafter\xyhashtag
\else
  \endgroup
\fi}

\newenvironment{post}[2]{%
  #1 said on #2:\quote
  \catcode`\#\active
  \catcode`\-11 %
  \catcode`\_11 %
  \lccode`\~`\#%
  \lowercase{\let~}\hashtag
}{%
  \endquote
}
\begin{post}{Sean Allred}{yesterday}
Help me, I'm #lost.

Help me, I'm #lost!

Best ever #tex #latex #plain_text

What? #confused#lost#saving-space
\end{post}
\end{document}

There are at least a couple of ways you could do this: catcode changes or the \lowercase 'trick'. As you've started off with catcodes, I'll stick with that. What you need to remember is you are tokenizing material when you do \newenviroment. As such, you need to change catcodes before the definition:

\documentclass{article}
\newcommand\maketag[1]{\##1}
\catcode`\"=6 %
\catcode`\#=\active
\newenvironment{post}{%
  \def#""1 {\maketag{""1}}%
  \catcode`\#=\active
  \quote
}{%
  \endquote
}
\catcode`\"=12 %
\catcode`\#=6 %

\begin{document}
\begin{post}
  #hello
\end{post}
\end{document}

Notice that inside the environment we only need to change the catcode of ": we need the definition to have " as the parameter char, but not to read the environment. It's only # that has special handling here. Also notice that as environments form groups there is no need to worry about resetting catcodes in the \endpost macro (end-of-environment argument).

For contrast, the \lowercase approach would look like

\documentclass{article}
\newcommand\maketag[1]{\##1}
\newenvironment{post}{%
  \begingroup
    \lccode`\~=\#
    \lowercase{%
      \endgroup
      \def~##1}{\maketag{##1}}%
  \catcode`\#=\active
  \quote
}{%
  \endquote
}

\begin{document}
\begin{post}
  #hello
\end{post}
\end{document}

Here, I don't have to mess with catcodes beyond making # active inside the environment. The idea here is that ~ is active anyway, so I can lower-case it into a # (which remains active) while using a 'normal' # for setting up the definition.

By the way, notice that in both cases we need to double the parameter char here as #1 (standard catcodes) refers to any argument for the environment itself. I didn't understand why you'd got [2] for arguments to that, so I dropped it!


Customizable at will (but of course rather slow):

\documentclass{article}
\usepackage{xcolor,environ,xparse,l3regex}

\ExplSyntaxOn
\NewDocumentEnvironment{post}{mm}
 {
  \char_set_catcode_other:N \# % change the catcode not to confuse \innerpost
  \char_set_catcode_other:N \_
  #1~said~on~#2\tl_to_str:n {:}\quote
  \innerpost % absorb the contents
 }
 {
  \endinnerpost\endquote
 }
\NewEnviron{innerpost}
 {
  \regex_replace_all:nnN
   { \#([[:word:]\-]*) }
   { \c{texttt}\cB\{\c{coloredhash}\1\cE\} }
   \BODY

  \BODY
 }
\ExplSyntaxOff

\newcommand\coloredhash{\textcolor{cyan}{\#}}

\begin{document}

\begin{post}{Sean Allred}{yesterday}
Help me, I'm #lost.

Help me, I'm #lost!

Best ever #tex #latex #plain_text

What? #confused#lost#saving-space
\end{post}

\end{document}

enter image description here

An example of further customization, suppose you want to accept accented characters in the hashtags, with \usepackage[utf8]{inputenc}; it's sufficient to change the search regular expression into

{ \#((\c[LA][^\~]|\_|\-)*) }

which matches any combination of letters, active characters (except for ~), the hyphen and the underscore.

A solution based on \ifcat would need much more tests.