How to repeat over all characters in a string?

You can use \@tfor. I provide also a better redefinition of the dot under according to your wish:

\documentclass{article}
\usepackage{graphicx}

\let\d\relax
\DeclareRobustCommand{\d}[1]{%
  \oalign{#1\cr\hidewidth\scalebox{0.5}{\textbullet}\hidewidth\cr}%
}
\makeatletter
\newcommand{\ds}[1]{%
  \@tfor\next:=#1\do{\d{\next}}%
}
\makeatother

\begin{document}

x\d{d}\d{s}\d{a}x

x\ds{dsa}x

\end{document}

enter image description here

What does \@tfor do? Its syntax is

\@tfor<scratch macro>:=<tokens>\do{<code>}

The scratch macro is traditionally \next, but it can be anything. The <tokens> part is any (brace balanced) list of tokens. In the loop, LaTeX essentially does \def<scratch macro>{<next token>}, so

\@tfor\next:=dsa\do{\d{\next}}

will perform

\def\next{d}\d{\next}\def\next{s}\d{\next}\def\next{a}\d{\next}

However, with \@tfor\next:=d {sa}\do{\d{\next}} we will just obtain

\def\next{d}\d{\next}\def\next{sa}\d{\next}

Explicit space tokens are ignored and braced groups of tokens are treated as one.

The expl3 analog is \tl_map_inline:nn:

\documentclass{article}
\usepackage{xparse}
\usepackage{graphicx}

\let\d\relax
\DeclareRobustCommand{\d}[1]{%
  \oalign{#1\cr\hidewidth\scalebox{0.5}{\textbullet}\hidewidth\cr}%
}

\ExplSyntaxOn
\NewDocumentCommand{\ds}{m}
 {
  \tl_map_inline:n { #1 } { \d { ##1 } }
 }
\ExplSyntaxOff

\begin{document}

x\d{d}\d{s}\d{a}x

x\ds{dsa}x

x\ds{d sa{bc}}x

\end{document}

No scratch macro is used: the current item in the loop is denoted by #1 (which becomes ##1 in the body of a definition, as usual).

In this particular case where just a single command is applied with the current item as argument, one can use \tl_map_function:nN:

\NewDocumentCommand{\ds}{m}
 {
  \tl_map_function:n { #1 } \d
 }

which has the same effect and is shorter. It can also appear in a full expansion context (not for this particular case, because of \d).

enter image description here


This solution allows word wrap and handles spaces between words. In addition, the [w] option allows the task to be performed on each word, rather than each character.'

In the MWE, I demonstrate with variously defined tasks:

  1. overstrike each character (2 different settings)

  2. place a dot under each character

  3. place a semicolon under each word

  4. apply extra intercharacter space

  5. apply extra interword space.

The task is defined in \charop{} for characters and \wordop{} for words, while the \chariterate[]{} macro does the iteration. Here is the MWE:

% WHILE THIS EXAMPLE IS SET UP FOR BOLDING A CALLIGRAPHIC FONT
% ITS GENERAL USE IS TO DO SOMETHING ON EACH char OF ITS ARGUMENT, ALLOWING LINE WRAP
% THE [W] OPTION TO \chariterate DOES SOMETHING TO EACH WORD.
\documentclass[10pt,a4paper,BCOR10mm,DIV11,toc=listof,parskip=full, openany]{scrbook}
\usepackage{stackengine}
\newcommand\chariterate[2][c]{\if w#1\worditeratehelper#2 \relax\relax\else
  \chariteratehelpA#2 \relax\relax\fi}
\def\chariteratehelpA#1 #2\relax{%
  \chariteratehelpB#1\relax\relax%
  \ifx\relax#2\else\ \chariteratehelpA#2\relax\fi
}
\def\chariteratehelpB#1#2\relax{%
  \charop{#1}%
  \ifx\relax#2\else
    \chariteratehelpB#2\relax%
  \fi
}
\def\worditeratehelper#1 #2\relax{%
  \wordop{#1}%
  \ifx\relax#2\else\ \worditeratehelper#2\relax\fi
}
\def\charop#1{#1}
\def\wordop#1{#1}
% THIS EXAMPLE ARTIFICIALLY BOLDS EACH char WITH A SHIFTED OVERSTRIKE
\def\charopA{%
  %\def\useanchorwidth{T}%
  \def\stacktype{L}%
  \def\stackalignment{l}%
  \def\calup{.2pt}%
  \def\calover{.15pt}%
  \renewcommand\charop[1]{\stackon[\calup]{##1}{\kern\calover##1}}%
}
% HERE'S AN EXAMPLE PLACING DOT UNDER EACH char
\def\charopB{%
  \def\useanchorwidth{T}%
  \def\stacktype{L}%
  \def\stackalignment{c}%
  \renewcommand\charop[1]{\stackunder[3pt]{##1}{.}}%
}
% EXTRA INTER-CHARACTER SPACE
\def\charopC{\renewcommand\charop[1]{##1\,}}
% HERE'S AN EXAMPLE PLACING DOT UNDER EACH WORD
\def\wordopA{%
  \renewcommand\wordop[1]{\def\stackalignment{c}\stackunder[3pt]{##1}{;}}
}
% EXTRA INTER-WORD SPACE
\def\wordopB{\renewcommand\wordop[1]{##1\ \ }}
%
\newenvironment{calligraphic}%
{\fontencoding{T1}\fontfamily{pzc}\fontseries{m}\fontshape{it}\fontsize{12pt}{12pt}\selectfont}{}%
\renewcommand*{\sectfont}{\normalcolor\usefont{T1}{pzc}{m}{it}}
\begin{document}
\charopA
   \begin{calligraphic}
   Test \textbf{this is not bold}\par
   Test \chariterate{this is bold with .2pt up shift and .15pt right shift}\par
 \def\calup{.0pt}
 \def\calover{.2pt}
   Test \chariterate{this is bold with .0pt up shift and .2pt right shift}\par
   \chariterate{cvxc This is a test. This is a test. This is a test. This is a test. 
    This is a test. This is a test. This is a test. This is a test. This is a 
    test. This is a test. This is a test. This fdsfsd is a test. This is a test. 
    This is a test. This is a test. This is adfsdf  test. This is a test.}\par
   \end{calligraphic}\par
\charopB
\wordopA
   \chariterate{This is doing something to each character.}\par
   \chariterate[w]{This is doing something to each word.}\par
   \chariterate{Ich bin m{\"u}de}.  
   \chariterate[w]{Ich bin m{\"u}de}.\par
\charopC
\wordopB
   \chariterate{This is doing something to each character.}\par
   \chariterate[w]{This is doing something to each word.}\par
\end{document}

enter image description here

In the most simple incarnation, to do merely underdots on characters alone, the code can be greatly reduced:

\documentclass{article}
\usepackage{stackengine}
\newcommand\chariterate[2][c]{\if w#1\worditeratehelper#2 \relax\relax\else
  \chariteratehelpA#2 \relax\relax\fi}
\def\chariteratehelpA#1 #2\relax{%
  \chariteratehelpB#1\relax\relax%
  \ifx\relax#2\else\ \chariteratehelpA#2\relax\fi
}
\def\chariteratehelpB#1#2\relax{%
  \charop{#1}%
  \ifx\relax#2\else
    \chariteratehelpB#2\relax%
  \fi
}
\def\worditeratehelper#1 #2\relax{%
  \wordop{#1}%
  \ifx\relax#2\else\ \worditeratehelper#2\relax\fi
}
  \def\useanchorwidth{T}%
  \def\stacktype{L}%
  \def\stackalignment{c}%
  \newcommand\charop[1]{\stackunder[3pt]{#1}{.}}%
  \def\wordop#1{#1}
\begin{document}
\chariterate{This is doing something to each character.
This is doing something to each character.
This is doing something to each character.}\par
\end{document}

The question is well suited for my new tokcycle package, so I just had to add another answer. The nice thing here, is that it can apply the underdot even as text style changes and macros are invoked. The \underdot is applied only to printable cat-11 tokens.

\documentclass{article}
\usepackage{tokcycle}
\newcommand\underdot[1]{\ooalign{#1\cr\hfil{\raisebox{-2.5pt}{.}}\hfil}}
\tokcycleenvironment\ds
  {\tctestifcatnx A##1{\addcytoks{\underdot{##1}}}{\addcytoks{##1}}}% CHARACTER DIRECTIVE
  {\processtoks{##1}}% GROUP-CONTENT DIRECTIVE
  {\addcytoks{##1}}% MACRO DIRECTIVE
  {\addcytoks{##1}}% SPACE DIRECTIVE
\begin{document}
\ds abc\endds

\ds This can work \textbf{across \bgroup\itshape changes in text
  style\egroup, and can |escape text, for example $y=2x+1$| and pick
  up where it left off}.  However, only catcode-11 tokens are underdotted.

It can continue across paragraphs.

This is from the new \textsf{tokcycle} package released on 2019/8/21.
\endds

\end{document}

enter image description here