1

I am a newbie in bash programming and was playing around with some commands trying to learn how passing multiple arguments using pipeline and grouping commands work when I faced this odd behavior which confused me a lot. I know other ways to achieve the what I want, however, I am trying to understand why this is happening.

I am trying to send a message to a user (let's consider their user ID as USER and their TTY as TTY) connected to my SSH server, using the write builtin function program.

While I was able to send the message just fine using the following command:

$ echo "some message" | write USER TTY

But when I was trying to pass USER and TTY using another pipeline, the message was not being sent:

$ echo "some message" | { echo "USER TTY" | xargs -o write; }

In the results, it seems the bash ignores the first part ($ echo "some message"), and need to enter the message after execution of the command.

Note that { echo "USER TTY" | xargs -o write; } and write USER TTY does the same job (apparently? I suspect there is a difference here that I am not aware of).

Again, I know that there are much easier ways to do this, but I just am trying to understand how bash works regarding grouping commands, piping, and passing input arguments to a function. Any comments on these areas are extremely appreciated.


For those who suspect I am asking this for a homework, which is nice to see people caring about these stuff, I genuinely was trying to create an alias that sends a message to every user on my ssh server, which I found wall does it way easier, though found it interesting to figure what I mentioned here out.

7
  • Please be honest, is this a homework or assignment question? Commented Mar 27, 2022 at 19:02
  • @ilkkachu Thank you for taking your time reading my question. You are very correct, I spent a whole day trying to find out a way to pass two arguments using pipeline without xargs, i.e. echo " source destination | mv but was not successful. As mentioned, I am very newbie to this area. However, I believe they do same job if you run these two commands Commented Mar 27, 2022 at 19:19
  • 1
    @SeetheMoar I am a neuroengineer interested in bash programming, you can simply search my name. BTW, I believe even if it was some sort of homework, the question still is valid since first of all I am asking how it works, ( and as I mentioned I know some other ways to achieve what I want), and second, others can learn too. Commented Mar 27, 2022 at 19:22
  • 3
    To be blunt, if you start from a misconception like “Command B | Command C does the same job as Command D” (which you could have easily checked, in isolation, without introducing Command A), and then you pile a complicated question on top of the initial misconception, and then you change the question halfway through (pulling xargs out of a hat), and you give it a title that’s down in the weeds of your example, rather than describing your question (such as it is), then nobody else is going to learn anything from it. Commented Mar 27, 2022 at 19:52
  • 3
    @MohammadBadriAhmadi, the first part of your question quite literally says "where Command B | Command C does the same job as Command D", with nothing there hinting any sort of doubt. But they don't do the same thing. Basing a question on a false premise rather makes it appear quite sloppy at best, and might not exactly help in getting a good answer. Neither does having the situation change in the middle of the post. Commented Mar 27, 2022 at 20:38

2 Answers 2

3

Your

echo "some message" | { echo "USER TTY" | xargs -o write; }

command can’t work because it does this:

                        |-------------------------------------------|
echo "some message"   --+->   echo "USER TTY"  -->  xargs -o write; |
                        |-------------------------------------------|

The standard input of the xargs is the pipe from echo "USER TTY".  The echo "some message" gets piped into the compound command { echo "USER TTY" | xargs -o write; }, which, practically, means that the echo "some message" gets piped into the echo "USER TTY"; which means that the write command cannot see the some message.
P.S. Since you have specified the -o option to xargs, write’s standard input is set to the terminal.  This would work if you wanted to say

echo "USER TTY" | xargs -o write

and then just type the message, but it doesn’t connect write to the outermost echo.

If you have some Command B that outputs USER and TTY (but just one pair), you could try

echo "some message" | write $(Command B)

Update: The below may work in the hypothetical case where Command B outputs just one pair of USER and TTY, but probably won’t work for your actual situation.  Read it, but then see the discussion further down.

If, as you say, you’re using bash, and you have GNU xargs (standard on Linux; probably available for other Unix-like operating systems) you could also try

echo "some message" | xargs -a <(Command B) write

as suggested by rici in Reading in from stdin from a command executed with xargs.

I just realized that you probably have GNU xargs, because the -o option isn’t specified by POSIX.


More thoughts:

  • This may address part of your question:  Offhand, I cannot think of any example where

    Command A | { Command B | Command C; }
    is different from
    Command A | Command B | Command C
    i.e., where the grouping makes a difference.  I expect that I am overlooking something obvious, and I look forward to having an example pointed out to me.  Likewise,
    { Command A | Command B; } | Command C
    would be equivalent.

  • Pipelines like

    Command A | Command B | Command C
    can be useful.  As a trivial example:

    $ echo GET | tr BEG VAC | od -cb
    0000000   C   A   T  \n
            103 101 124 012
    0000004
    
  • A pipeline like

    Command A | Command B | xargs (options)  Command Y
    can be useful if you are thinking of it as
    { Command A | Command B; }  |  xargs (options)  Command Y
    i.e., Command A | Command B provides the input to xargs.  If you’re thinking of it as
    Command A  |  { Command B | xargs (options)  Command Y; }
    i.e., Command B provides the input to xargs and Command A provides the input to Command Y, it’s not going to work for the reason discussed above: there’s no way to establish a data flow between Command A and Command Y.

  • However,

    Command A  |  xargs (options)  -a <(Command B)  Command Y
    is problematic for a different reason: the job of xargs here is to run Command Y (potentially) multiple times.  But there’s no mechanism to run Command A multiple times, so it cannot provide the input to Command Y multiple times.

    If you really want to run Command A | Command Y multiple times, you can do that with

    Command B | xargs (options)  sh -c 'Command A | Command Y' sh
    For example,

    $ echo S F u o n b | xargs -n2 sh -c 'date | tr "$1" "$2"; sleep 2' sh
    Fun, Mar 27, 2022 11:11:07 AM
    Son, Mar 27, 2022 11:11:09 AM
    Sub, Mar 27, 2022 11:11:11 AM
    

    This runs

    date | tr S F
    date | tr u o
    date | tr n b
    

    You can see that it is actually running date three times from the fact that the seconds are different (07, 09 and 11).

    And this will work for your case.  (Note that, in my example above, Command Y takes arguments from Command B two at a time, like what you really want/need to do with write.)  If Command B produces output that looks like name1 tty1 name2 tty2 name3 tty3 … (e.g., fred tty17 wilma tty42 barney ttyS23), then you can do

    Command B | xargs -n2 (maybe other options)  sh -c 'echo "some message" | write "$1" "$2"' sh

  • But, if you don’t need to run Command A (in your case, echo "some message") multiple times (because it produces the same output every time), you could make use of that fact, and run Command A once, saving the output:

    echo "some message" > tmpfile; \
            Command B | xargs -n2 (maybe other options)  sh -c 'write "$1" "$2" < tmpfile' sh; \
            rm tmpfile

  • So, the above solution avoids running Command A multiple times, but it still runs the shell once for each user.  This is undesirable.  I don’t see any way to handle this scenario, with xargs, without firing up the shell multiple times.  (Again, I anticipate having one pointed out to me.)  It’s hard to handle it without using the shell at all, because xargs can handle only ‘‘simple’’ commands — no redirections (<) or pipes (|).  But, we can handle it without xargs, using the shell more creatively.  Assuming that Command B produces output with one namen ttyn pair per line (e.g., fred tty17 (newline) wilma tty42 (newline) barney ttyS23 (newline) ), then you can do

    echo "some message" > tmpfile; \
                  Command B | while read u t; do write "$u" "$t" < tmpfile; done; \
                  rm tmpfile


To get down into the weeds and address a couple of micro-points from the question:

  • In

    echo "some message" | { echo "USER TTY" | xargs -o write; }
    the shell isn’t actually ignoring the echo "some message" part, per se.  But, as I described in the first part of my answer, it is effectively piping it into the echo "USER TTY"part.  And echo doesn’t read its standard input; therefore, the message isn’t processed.

  • Note that { echo "USER TTY" | xargs -o write; } and write USER TTY do the same job (apparently? I suspect there is a difference here that I am not aware of).

    OK, in case this isn’t clear yet: both variants invoke the write program with arguments USER and TTYwrite USER TTY is a simple command that doesn’t modify its standard I/O streams, but simply reads from the existing stdin.  This could be the tty, or a pipe, or a file.  The xargs variant redirects write’s stdin to be /dev/tty, because that’s what -o does.  Normally (if you don’t specify -a), xargs’s stdin is the source of the arguments for the command to be executed, and so xargs doesn’t have access to the prevailing stdin in the larger environment (which would be the source of the message for write) — and therefore it cannot make that data stream available to the executed program.  So, if you don’t specify -a or -oxargs sets the stdin of the executed program to /dev/null.

  • Note that

    command B | xargs -n2 -o write
    would sort-of work, but it would require you to type the input message once for each user, because it invokes write once for each user without having any way to save the message between iterations.  You could theoretically use this if you wanted to send different messages to different users, but it wouldn’t be practical, because it wouldn’t give you any prompts or feedback to tell you which user you were talking to.

6
  • First of all, Thank you so much for your detailed answer. Second, you are totally correct about the first part ( and second part as well), I should have mentioned it in the question. The problem is, I tried a lot to find Command A and Command B in a way that Command A | Command B becomes the same as write USER TTY. However, I was not able to do it without using xargs. Regarding the last part of your answer, I totally understand it where I made a mistake now thanks to your explanation, however, do you think it is possible to do what I was trying ? Commented Mar 27, 2022 at 19:58
  • Also, I am sorry for not being able to vote to your answer, I am new here. However, what I meant by "what I was trying" is, basically piping echo "some message into the final result of echo "USER TTY" | xargs -o write, similar to echo "some message" | write USER TTY Commented Mar 27, 2022 at 20:01
  • 2
    @MohammadBadriAhmadi if you tell us in your question what you're trying achieve then we're likely to be able to provide a solution. Step away from generalities - they're great up to a point but from there on you need to talk specifics (if you can). It might be worth accepting one of these answers and then opening a new question that builds on what you have here but tells about the specific Commented Mar 27, 2022 at 20:44
  • 1
    If you liked that,  here — have some more. Commented Mar 28, 2022 at 20:19
  • 1
    @G-ManSays'ReinstateMonica' I even cannot express how thankful I am. You cannot imagine how much easier you made it for me. I've been looking for this information for a long time and I would have. Cheers man. Commented Mar 28, 2022 at 21:18
-1

First Point

  • You cannot use echo "<command arguments>" | command because you're echoing arguments as input. If you want to use input for command arguments, you have to use xargs as a "translator" for converting input into command line arguments.

xargs is an external binary which is not part of the shell(i.e. bash). There are many versions of xargs but the version with the -o flag is found in the GNU userspace. Generally, on linux, you can use man <command name> to find out documentation about a particular command. From a web version of the manual:

This manual page documents the GNU version xargs. xargs reads items from the standard input, delimited by blanks (which can be protected with double or single quotes or a backslash) or newlines, and executes the command (default is echo) one or more times with any initial-arguments followed by items read from standard input.

What xargs does is, it breaks the inputs into fields, think of a row in a table. For each field, (by default) xargs passes an argument to the program until a system defined limit is reached. Think of spaces and newlines as the separator, i.e. the | in a table. Since your input "USER TTY" has a space between USER and TTY, xargs creates something similar to a table row like so

ARG1 ARG2
USER TTY

Second Point

  • If you want to pipe a file everytime a command is run, use xargs to pipe a file to the run command like so. Beware that this opens a new shell however, so environment variables from your current session cannot be used.
echo "USER TTY" | xargs -I REPL sh -c 'echo "some message" | write REPL'

Breakdown: -I flag takes an argument REPL that is replaced by the field when the command is run.

USER TTY

Therefore xargs will run

sh -c 'echo "some message" | write USER TTY'
7
  • 1
    "For each field, the given command is executed once.", no this would only apply with xargs -n 1. But it does split the input string USER TTY to the two arguments USER and TTY, and you're right it does interpret quotes itself (which actually makes it harder to use safely), so the input "USER TTY" (or a variant) would result in the single argument USER TTY. But we probably don't want that with write. Commented Mar 27, 2022 at 20:21
  • @SeeTheMoad I believe you made a mistake regarding xargs, as far as I know ( again, a newbie) it splits the string into input variables. and also, can you explain more about circumvent you mentioned? cause I piped all three cases you mentioned to write and none worked. Commented Mar 27, 2022 at 20:24
  • @ikkachu explained it way better than me Commented Mar 27, 2022 at 20:25
  • @MohammadBadriAhmadi See GMAN's answer first, it mentions your first mistake. You can't pipe command line arguments to a program as input. You must use xargs to translate piped input into command line arguments Commented Mar 27, 2022 at 20:38
  • @SeetheMoar: I appreciate being given credit, but I’m not sure I like you mentioning my name.  Your answer “adds to” mine like dropping a football on the dinner table adds to the meal.  And I was going to say what ilkkachu said, but they beat me to it. Commented Mar 27, 2022 at 20:44

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.