You can, e.g., create a command which has LaTeX temporarily change the catcode of space, tab, return and % (% is used for commenting) and via a variant of \kernel@ifnextchar (which itself is based on \futurelet) check the next token in the token-stream.
Some days ago, in my answer to the question Space after LaTeX commands, I tried to explain the drawbacks of the approach of having LaTeX "look ahead" at the next token:
The major drawback of this method is that it relies on the next token in the token-stream coming into being by reading and tokenizing tex-input from the tex-source-code while the temporary changes of these catcodes are effective.
But tokens can also get into the token-stream not by reading and tokenizing tex-input from the tex-source-code but as a result of expanding, e.g., a macro-token where both the replacement-text and the arguments got tokenized at points in time when these category-codes were not changed.
Commands that temporarily change the category-code-régime and rely on the changed category-code-régime being in effect when tokens which they shall process get tokenized cannot be used in circumstances where the things they shall process will already have been tokenized under the unchanged category-code-régime as would be the case, e.g., when they get their arguments passed by other macros as a result of expanding these other macros.
Therefore with the example below, \XXX is defined in terms of \outer for ensuring as good as possible that it will not be used within the definition-texts or the arguments of other macros.
You need another variant of \kernel@ifnextchar because you cannot safely use \kernel@ifnextchar as \kernel@ifnextchar is definitely not 100%ly reliable:
The commented sources of LaTeX 2e as a pdf-file whose name is source2e.pdf can be found at http://mirrors.ctan.org/macros/latex/base/source2e.pdf.
\kernel@ifnextchar in the LaTeX 2e sources is defined as follows—File d: ltdefns.dtx Date: 2018/09/26 Version v1.5e :
321 \long\def\@ifnextchar#1#2#3{%
322 \let\reserved@d=#1%
323 \def\reserved@a{#2}%
324 \def\reserved@b{#3}%
325 \futurelet\@let@token\@ifnch}
326 \let\kernel@ifnextchar@ifnextchar
327 \def@ifnch{%
328 \ifx@let@token@sptoken
329 \let\reserved@c@xifnch
330 \else
331 \ifx@let@token\reserved@d
332 \let\reserved@c\reserved@a
333 \else
334 \let\reserved@c\reserved@b
335 \fi
336 \fi
337 \reserved@c}
338 \def:{\let@sptoken= } : % this makes @sptoken a space token
339 \def:{@xifnch} \expandafter\def: {\futurelet@let@token@ifnch}
E.g., you said you cannot test for a space. That's true. There was extra effort for implementing a loop that does remove spaces when implementing \kernel@ifnextchar/\@ifnextchar.
This loop leads to error-messages with things like:
\kernel@ifnextchar{⟨char⟩}{The next thing is ⟨char⟩}{The next thing is not ⟨char⟩}\@sptoken
That has to do with the fact that knowing whether tokens have the same meaning does not imply knowing whether they are the same tokens.
When the token trailing the arguments of \kernel@ifnextchar has the meaning of the token \reserved@d, i.e., when you have something like
\kernel@ifnextchar{⟨char⟩}{The next thing is ⟨char⟩}{The next thing is not ⟨char⟩}\reserved@d
, you get \kernel@ifnextchar's second argument no matter what its first argument is.
(That's why things like \reserved@d are reserved. ;-) )
See, e.g., what you get from
\documentclass{article}
\makeatletter
\begin{document}
% This is nice:
\kernel@ifnextchar{X}{The next thing is X}{The next thing is not X}\reserved@d
\kernel@ifnextchar{Y}{The next thing is Y}{The next thing is not Y}\reserved@d
\kernel@ifnextchar{Z}{The next thing is Z}{The next thing is not Z}\reserved@d
\kernel@ifnextchar{\LaTeX}{The next thing is \LaTeX}{The next thing is not \LaTeX}\reserved@d
% This raises nice errors.
% \kernel@ifnextchar{X}{The next thing is X}{The next thing is not X}\@sptoken X
\end{document}

The reason for this is that \kernel@ifnextchar does not distinguish different tokens from each other which have the same meaning. As a special case of this behavior, \kernel@ifnextchar does not distinguish implicit characters from explicit characters.
See, e.g., what you get from
\documentclass{article}
\makeatletter
\let\implicitA=A
\begin{document}
\kernel@ifnextchar{A}{We have an }{We don't have an }\implicitA
\kernel@ifnextchar{\implicitA}{We have an }{We don't have an }A
\end{document}

Above it was said:
You can, e.g., create a command which has LaTeX temporarily change the catcode of space, tab, return and % (% is used for commenting) and via a variant of \kernel@ifnextchar (which itself is based on \futurelet) check the next token in the token-stream.
If the catcode of these characters is switched to 12(other) and checking only for these characters is of interest, you can have LaTeX peek at the meaning of the next token via \futurelet while leaving that token in place.
In case the meaning of the next token equals the meaning of one of these explicit catcode-12-character-tokens, you can safely have LaTeX grab that token as an undelimited macro argument. Thus in this special case of checking for explicit catcode-12-characters you can have LaTeX "grab" the token itself for defining a temporary macro that expands to that token. If there also is already defined another temporary argument that expands to your \kernel@ifnextchar-variant's first argument, LaTeX can do an \ifx-comparison with these temporary macros for making sure that the two tokens in question do not just have the same meanings but are really the same tokens. (When grabbing the next token as argument, LaTeX should probably put it back in the right moment...)
In the example below I tried to implement a routine \UD@ifnextcharForOtherTokens which causes LaTeX to do these things. At least I hope it does. ;-)
\documentclass{article}
\makeatletter
% Patch verbatim to also display horizontal tabs:
% The patch is needed for this example only.
\usepackage{amssymb}
\g@addto@macro\dospecials{\keystroketab}
\newbox\UD@tempbox
\begingroup
\catcode`\^^I=13\relax
\@firstofone{%
\endgroup
\newcommand\keystroketab{%
\setbox\UD@tempbox\hbox{\verbatim@font\char32}%
\catcode`\^^I=13\relax
\def^^I{\mbox{\hbox to 3\wd\UD@tempbox{\null\hfill$\leftrightarrows$\hfill\null}}}%
}%
}%
%verbatim-patch done.
% \UD@ifnextcharForOtherTokens peeks at the following token and
% compares it with its first argument w h i c h m u s t b e
% a s i n g l e t o k e n a n d w h i c h i n c a s e
% o f b e i n g a c h a r a c t er t o k e n --- b e
% i t e x p l i c i t o r i m p l i c i t --- m u s t
% b e a c h a r a c t e r t o k e n w h o s e e x p l i c i t
% v a r i a n t c a n b e s a f e l y g r a b b e d a s
% u n d e l i m i t e d a r g u m e n t!!! T h i s i s t h e
% c a s e, e. g., w i t h c h a r a c t er t o k e n s o f
% c a t e g o r y c o d e 12(other)!
% If both are the same it executes its second argument, otherwise
% its third.
\newcommand\UD@reserved@a{}%
\newcommand\UD@reserved@b{}%
\newcommand\UD@reserved@c{}%
\newcommand\UD@reserved@d{}%
\newcommand\UD@let@token{}%
\newcommand\UD@ifnextcharForOtherTokens[3]{%
\begingroup
\def\UD@reserved@d{#1}%
\def\UD@reserved@a{#2}%
\def\UD@reserved@b{#3}%
\futurelet\UD@let@token\UD@ifnch
}%
\newcommand\UD@ifnch{%
\expandafter\ifx\expandafter\UD@let@token\UD@reserved@d
\expandafter\UD@ifnchsnapnexttoken
\else
\expandafter\expandafter\expandafter
\endgroup\expandafter\UD@reserved@b
\fi
}%
\newcommand\UD@ifnchsnapnexttoken[1]{%
\def\UD@reserved@c{#1}%
\ifx\UD@reserved@c\UD@reserved@d
\expandafter\expandafter\expandafter
\endgroup\expandafter\UD@reserved@a
\else
\expandafter\expandafter\expandafter
\endgroup\expandafter\UD@reserved@b
\fi
#1%
}%
\begingroup
% -------------------------------------------------
% Don't indent the following code!
% Each line of the following code must end with ^^A
% vwhich serves as comment-char!
\catcode`\^^A=14\relax%
\catcode`\ =12\relax^^A
\catcode`\^^I=12\relax^^A
\catcode`\^^M=12\relax^^A
\catcode`\%=12\relax^^A
\@firstofone{^^A
\endgroup^^A
\newcommand\UD@removeotherspace{}^^A
\long\def\UD@removeotherspace#1 {#1}^^A
\newcommand\UD@removeotherreturn{}^^A
\long\def\UD@removeotherreturn#1^^M{#1}^^A
\newcommand\UD@removeothertab{}^^A
\long\def\UD@removeothertab#1^^I{#1}^^A
\newcommand\UD@removecomment{}^^A
\long\def\UD@removecomment#1#2%#3^^M{\endgroup\UD@removecommentloop{#1}{#2}}^^A
\begingroup^^A
\newcommand\UD@CheckWhetherSourceSpace[1]{^^A
\endgroup^^A
\newcommand\UD@removecommentloop[2]{^^A
\UD@ifnextcharForOtherTokens{ }{\UD@removeotherspace{\UD@removecommentloop{##1}{##2}}}{^^A
\UD@ifnextcharForOtherTokens{^^I}{\UD@removeothertab{\UD@removecommentloop{##1}{##2}}}{^^A
\UD@ifnextcharForOtherTokens{%}{\begingroup\catcode`\^=12#1\UD@removecomment{##1}{##2}}{^^A
\UD@ifnextcharForOtherTokens{^^M}{\UD@removeotherreturn{\endgroup##1{\par}}}{\endgroup##2}^^A
}}}}^^A
\newcommand\UD@CheckWhetherSourceSpace[2]{^^A
\begingroup^^A
\catcode`\ =12\relax^^A
\catcode`\^^I=12\relax^^A
\catcode`\^^M=12\relax^^A
\catcode`\%=12\relax^^A
\UD@ifnextcharForOtherTokens{%}{\begingroup\catcode`\^=12#1\UD@removecomment{##1}{##2}}{^^A
\UD@ifnextcharForOtherTokens{ }{\endgroup\UD@removeotherspace{##1{#1\ignorespaces}}}{^^A
\UD@ifnextcharForOtherTokens{^^M}{\endgroup\UD@removeotherreturn{##1{#1\ignorespaces}}}{^^A
\UD@ifnextcharForOtherTokens{^^I}{\endgroup\UD@removeothertab{##1{#1\ignorespaces}}}{\endgroup##2}^^A
}}}}}^^A
}%<- closing brace of \@firstofone's argument
\UD@CheckWhetherSourceSpace{ }%
%
% Now we are back to normal circumstances. ;-)
% -------------------------------------------------
%
% !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
% Commands that call \UD@CheckWhetherSourceSpace must be defined in
% terms of \outer.
% They may be used only in circumstances where their arguments get
% read and tokenized _while_ they are carried out.
% They must not be used in circumstances where the tokens probably
% forming their arguments already got tokenized and then were passed
% on to them via "spitting out" some macro-definition or
% macro-argument or the like.
% Be aware that defining in terms of \outer does not prevent
% erroneous usage to 100%,
% !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
%
% The first argument of \UD@CheckWhetherSourceSpace is a macro that
% performs the action in case no argument is present. It seems
% confusing but it must nonetheless process one argument.
% That argument is deliveredby \UD@CheckWhetherSourceSpace as a means
% of providing info about whether a space token or a \par-token
% needs to be appended after the action.
% The second argument of \UD@CheckWhetherSourceSpace is a macro that
% performs the action in case an argument is present.
\newcommand\XXX{}
\outer\def\XXX{%
\UD@CheckWhetherSourceSpace{\XXX@Space}{\XXX@Arg}%
}%
\newcommand\XXX@Space[1]{\fbox{Bye}#1}%
\newcommand\XXX@Arg[1]{\fbox{Hello #1}}
\makeatother
\parindent=-.66cm
\begin{document}
{\bfseries Linebreak between \verb|\XXX| and following stuff:}
\emph{The code}
\begingroup\topsep=0ex\partopsep=0ex
\begin{verbatim*}
text \XXX
1 A
\end{verbatim*}%
\endgroup
\emph{yields:}\\
text \XXX
1 A
\emph{The code}
\begingroup\topsep=0ex\partopsep=0ex
\begin{verbatim*}
text \XXX
{1} A
\end{verbatim*}
\endgroup
\emph{yields:}\\
text \XXX
{1} A
\vfill
{\bfseries Comment and linebreak between \verb|\XXX| and following stuff:}
\emph{The code}
\begingroup\topsep=0ex\partopsep=0ex
\begin{verbatim*}
text \XXX%
% Comment ^^M^^M Comment
% Comment
1 A
\end{verbatim*}
\endgroup
\emph{yields:}\\
text \XXX%
% Comment ^^M^^M Comment
% Comment
1 A
\vfill
{\bfseries Comment and linebreak and linebreak between \verb|\XXX| and following stuff:}
\emph{The code}
\begingroup\topsep=0ex\partopsep=0ex
\begin{verbatim*}
text \XXX%
% Comment ^^M^^M Comment
% Comment
1 A
\end{verbatim*}
\endgroup
\emph{yields:}\\
\begingroup\parindent=0ex
text \XXX%
% Comment ^^M^^M Comment
% Comment
1 A
\endgroup
\vfill
{\bfseries Space between \verb|\XXX| and following stuff:}
\verb*|text \XXX 1 A|: text \XXX 1 A
\verb*|text \XXX {1} A|: text \XXX {1} A
\vfill
{\bfseries Horizontal tab between \verb|\XXX| and following stuff:}
\verb*|text \XXX 1 A|: text \XXX 1 A
\verb*|text \XXX {1} A|: text \XXX {1} A
\vfill
{\bfseries Nothing between \verb|\XXX| and following stuff:}
\verb*|text \XXX1 A|: text \XXX1 A
\verb*|text \XXX{1} A|: text \XXX{1} A
\verb*|text \XXX123 A|: text \XXX123 A
\verb*|text \XXX{123} A|: text \XXX{123} A
\vfill\vfill\vfill\vfill\vfill\vfill
\end{document}

For some reason unknown to me horizontal-tabs in examples get transformed into four spaces when pasting code into the "Answer"-window of StackExchange.
Thus the horizontal-tabs I typed into the example before compiling it may during the copy-paste-process have been transformed to spaces which implies that the result of copy-pasting the example from StackExchange and compiling may have a look which is slightly different from the look of the image above.
I state clearly and explicitly that I do not recommend this approach. It has too many drawbacks and restrictions. The example is intended to show how confusing (La)TeX-programming can be. ;-)
One restriction is already mentioned: You can't use \XXX within macro-definitions or within macro-arguments. You'd better not have other macros deliver \XXX's argument or tokens that shall be behind \XXX but not be taken for \XXX's argument.
Some other restrictions and drawbacks are:
\XXX performs a lot of temporary assignments. Thus \XXX is not fully expandable and cannot be used safely in pure-expansion-contexts.
A thing like \XXX could not be used safely within moving-arguments, i.e., within things that, e.g., get written to temporary files so that in future LaTeX-runs they can pop up in the table of contents or within \label-\ref-cross-references also. One of the reasons for this is that LaTeX does always attach a space character when unexpanded-writing a control-word-token to an external text-file.
E.g.,
\newwrite\mywrite
\immediate\openout\mywrite experiment.tex %
\immediate\write\mywrite{\noexpand\XXX{Something}}%
\immediate\closeout\mywrite
yields a file experiment.tex whose content is:
\XXX␣{Something}
Note the space character between \XXX and {Something}.
[1]and[2]instead of1and2, you could do\documentclass{article} \begin{document} \newcommand{\XXX}[1][\empty]{\ifx#1\empty Bye \else Hello #1 \fi} \XXX[1] \XXX \end{document}. – Feb 11 '19 at 22:29\XXXin general? If you want the "mandatory" argument to be conditional, then there might be instances where you're forced to introduce some weird syntax so as to not grab content. For example, will\XXXonly accept numbers, or characters as well? If numbers, single or double digits or higher order digits? Will you use input like\XXX1,\XXX{1}and\XXX 1interchangeably? – Werner Feb 11 '19 at 22:39\XXXby itself?\XXX1is the same as\XXX 1so what input following\XXXwould you consider as terminating the construct with no argument? – David Carlisle Feb 11 '19 at 23:01\else, but try\XXX[11]... – David Carlisle Feb 11 '19 at 23:06\XXX1. On the other hand you say: "I do not care how\XXX 1behaves." Be aware that under normal conditions reading and tokenizing the input-sequence\XXX1yields the same set of tokens as reading and tokenizing the input-sequence\XXX 1, i.e., the control-word-token\XXXand the explicit catcode-12-character-token1. – Ulrich Diez Feb 12 '19 at 01:53\XXX 1would act as\XXX1. However, if your solution breaks "normal conditions", it is OK by me. However, these kind of question is precisely what made me abandon writing such a macro --- it will be too fragile; my coauthor or I might forget its quirk. – Boris Bukh Feb 12 '19 at 01:57