It's no different from usual TeX programming. In this case we can exploit the possibility of making a character math active and the advanced management of active characters provided by expl3
.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\cs_new:Npn \sean_active_define:NN #1 #2
{
% define the active version of character #1 to do #2
\char_set_active_eq:nN { `#1 } #2
% make #1 math active
\char_set_mathcode:nn { `#1 } { "8000 }
}
\NewDocumentCommand \Digits { m m }
{
% limit the scope of the math active characters
\group_begin:
\sean_active_define:NN ; \__sean_active_semicolon:
\sean_active_define:NN . \__sean_active_period:
\langle #1 \rangle\sb{#2}
\group_end:
}
\cs_new_protected:Npn \__sean_active_semicolon: { ,\; }
\cs_new_protected:Npn \__sean_active_period: { \dots }
\ExplSyntaxOff
\begin{document}
\makebox[3cm][l]{Desired syntax:}
\verb$\Digits{1,2,3;4,5,6;.;7}{8}$
\makebox[3cm][l]{Obtained output:}
$\Digits{1,2,3;4,5,6;.;7}{8}$
\makebox[3cm][l]{Desired output:}
$\langle 1,2,3, \; 4,5,6, \; \dots, \; 7 \rangle_8$
\end{document}

Note. The most recent version (September 2015) of the l3 kernel has improved the management of active characters.
OLD VERSION
It's no different from usual TeX programming; category codes are fixed at the moment the definition is stored, and in your definition ;
and .
have category code 12.
The procedure requires using \tl_to_lowercase:n
in order to obtain the requested characters as active, while being able to use them in the replacement text.
Here's a possible implementation, with a generic activation
function; the definition of \Digits
shouldn't have parameters, in order to avoid absorbing the arguments after activating the characters.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\group_begin:
\char_set_catcode_active:N \^^@
\cs_new:Npn \sean_active_define:NN #1 #2
{
\group_begin:
\char_set_lccode:nn { `^^@ } { `#1 }
\tl_to_lowercase:n
{
\group_end:
\cs_set_eq:NN ^^@
}
#2 % the replacement text is better outside \tl_to_lowercase:n
\char_set_catcode_active:N #1
}
\group_end:
\NewDocumentCommand \Digits { }
{
\group_begin:
\sean_active_define:NN ; \__sean_active_semicolon:
\sean_active_define:NN . \__sean_active_period:
\sean_digits:nn
}
\cs_new_protected:Npn \sean_digits:nn #1 #2
{
\langle #1 \rangle\sb{#2}
\group_end:
}
\cs_new_protected:Npn \__sean_active_semicolon: { ,\; }
\cs_new_protected:Npn \__sean_active_period: { \dots }
\ExplSyntaxOff
\begin{document}
\makebox[2.5cm][l]{Desired syntax:}
$\Digits{1,2,3;4,5,6;.;7}{8}$
\makebox[2.5cm][l]{Desired output:}
$\langle 1,2,3, \; 4,5,6, \; \dots, \; 7 \rangle_8$
\end{document}

You may avoid activating the characters by making them “math active”, which doesn't require absorbing the arguments in two stages.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\group_begin:
\char_set_catcode_active:N \^^@
\cs_new:Npn \sean_math_active_define:NN #1 #2
{
\group_begin:
\char_set_lccode:nn { `^^@ } { `#1 }
\tl_to_lowercase:n
{
\group_end:
\cs_set_eq:NN ^^@
}
#2 % the replacement text is better outside \tl_to_lowercase:n
\char_set_mathcode:nn { `#1 } { "8000 }
}
\group_end:
\NewDocumentCommand \Digits { m m }
{
\group_begin:
\sean_math_active_define:NN ; \__sean_active_semicolon:
\sean_math_active_define:NN . \__sean_active_period:
\langle #1 \rangle\sb{#2}
\group_end:
}
\cs_new_protected:Npn \__sean_active_semicolon: { ,\; }
\cs_new_protected:Npn \__sean_active_period: { \dots }
\ExplSyntaxOff
\begin{document}
\makebox[2.5cm][l]{Desired syntax:}
$\Digits{1,2,3;4,5,6;.;7}{8}$
\makebox[2.5cm][l]{Desired output:}
$\langle 1,2,3, \; 4,5,6, \; \dots, \; 7 \rangle_8$
\end{document}
NOTE: I changed slightly the implementation, making it a bit more powerful and easier to manage; now it's easy to define local active characters also with arguments.
\NewDocumentCommand\Digits{}{(catcode stuff)\real_digits:nn
? See edit.\tl_replace_all:Nnn
, which is “easier” (at least to me). Is there a reason why this wouldn't work?