29

How can I perform two commands on one input, without typing that input twice?

For example, the stat command tells a lot about a file, but doesn't indicate its file type:

stat fileName

The file command, tells what type a file is:

file fileName

You can perform this in one line this way:

stat fileName ; file fileName

However, you have to type the fileName twice.

How can you execute both commands on the same input (without typing the input or a variable of the input twice)?

In Linux, I know how to pipe outputs, but how do you pipe inputs?

6
  • 14
    Just to be clear, those aren't "inputs", they're arguments (aka parameters). Input can be piped via < (input from file to the left side) or | (input from stream to the right side). There's a difference.
    – goldilocks
    Commented Jun 11, 2014 at 14:46
  • 6
    Not an answer to your question, but maybe an answer to your problem: In bash, the yank-last-arg command (default shortcut Meta-. or Meta-_; Meta often being the left Alt key) will copy the last parameter from the preceding command line into the current one. So, after executing stat fileName you type file [Meta-.] and do not have to type that fileName again. I always use that.
    – Dubu
    Commented Jun 12, 2014 at 10:18
  • 2
    And the difference is that < and > are redirection, not piping. A pipeline connects two processes, while redirection simply reassigns stdin/stdout.
    – bsd
    Commented Jun 12, 2014 at 18:47
  • @bdowning: Yeah, but you're redirecting the pipe though aren't you. You can redirect the start of a pipeline to be reading from a file on disk, or you can redirect the end of a pipeline to be writing to a file on disk. Still piping. ;-) Commented Jun 13, 2014 at 15:43
  • 1
    Learn to define functions in your shell. Commented Aug 27, 2015 at 6:45

10 Answers 10

34

Probably going to get my knuckles rapped for this, here's a hacky combination of bash brace expansion and eval that seems to do the trick

eval {stat,file}" fileName;"
5
  • 10
    Sorry, but I cannot resist. Commented Jun 11, 2014 at 14:54
  • 2
    brace expansion originated in csh, not bash. bash copied it from ksh. That code will work in all of csh, tcsh, ksh, zsh, fish and bash. Commented Jun 11, 2014 at 14:54
  • 1
    Too bad you lose the ability to "tab out" (auto-complete) the fileName, due to the quote. Commented Jun 27, 2014 at 18:35
  • Sorry, but I could not resist either
    – tomd
    Commented Feb 2, 2016 at 19:11
  • sorry what's the problem here with eval? (referring to top comments)
    – user13107
    Commented Mar 11, 2016 at 6:56
30

With zsh, you can use anonymous functions:

(){stat $1; file $1} filename

With es lambdas:

@{stat $1; file $1} filename

You could also do:

{ stat -; file -;} < filename

(doing the stat first as the file will update the access time).

I'd do:

f=filename; stat "$f"; file "$f"

though, that's what variables are for.

6
  • 3
    { stat -; file -;} < filename is magic. stat must be checking to see that its stdin is connected to a file and reporting the file's attributes? Presumably not advancing the pointer on stdin therefore allowing file to sniff stdin contents
    – iruvar
    Commented Jun 11, 2014 at 16:22
  • 1
    @1_CR, yes. stat - tells stat to do an fstat() on its fd 0. Commented Jun 11, 2014 at 17:32
  • 4
    The { $COMMAND_A -; $COMMAND_B; } < filename variant won't work in the general case if $COMMAND_A actually reads any input; e.g. { cat -; cat -; } < filename will print the contents of filename only once.
    – Max Nanasy
    Commented Jun 11, 2014 at 22:30
  • 2
    stat - is handled specially since coreutils-8.0 (Oct 2009), in versions prior to that you will get an error (unless you have a file called - from a previous experiment, in which case you'll get the wrong results...) Commented Jun 12, 2014 at 11:03
  • 1
    @mr.spuratic. Yes, and BSDs, IRIX and zsh stats won't recognise it either. With zsh and IRIX stat, you can use stat -f0 instead. Commented Jun 12, 2014 at 11:43
24

Here is another way:

$ stat filename && file "$_"

Example:

$ stat /usr/bin/yum && file "$_"
  File: ‘/usr/bin/yum’
  Size: 801         Blocks: 8          IO Block: 4096   regular file
Device: 804h/2052d  Inode: 1189124     Links: 1
Access: (0755/-rwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Context: system_u:object_r:rpm_exec_t:s0
Access: 2014-06-11 22:55:53.783586098 +0700
Modify: 2014-05-22 16:49:35.000000000 +0700
Change: 2014-06-11 19:15:30.047017844 +0700
 Birth: -
/usr/bin/yum: Python script, ASCII text executable

That works in bash and zsh. That also works in mksh and dash but only when interactive. In AT&T ksh, that only works when the file "$_" is on a different line from the stat one.

2
  • 1
    The !$ one won't work because !$ expands to the last word of the last history event (so of the previously entered command line, not of the stat command). Commented Jun 11, 2014 at 17:35
  • @StéphaneChazelas: Oh, yes, my mistake, I deleted it.
    – cuonglm
    Commented Jun 11, 2014 at 17:37
11

All of these answers seem like scripting to me, to a greater or lesser extent. For a solution that really involves no scripting, and assumes that you're sitting at the keyboard typing into a shell, you can try:

stat somefile Enter
file Esc_   Enter

In bash, zsh, and ksh at least, in both vi and emacs modes, pressing Escape and then underscore inserts the last word from the previous line into the edit buffer for the current line.

Or, you can use history substitution:

stat somefile Enter
file !$ Enter

In bash, zsh, csh, tcsh, and most other shells, !$ is replaced with the last word of the previous command line when the current command line is parsed.

4
  • what other options are available in zsh/bash that are like Esc _ ? Can you please link with the official documentation? Commented Jun 17, 2014 at 3:12
  • 1
    Bash: gnu.org/software/bash/manual/bashref.html#Command-Line-Editing and search for "M-_"
    – godlygeek
    Commented Jun 17, 2014 at 17:17
  • 1
    zsh: linux.die.net/man/1/zshzle and search for "insert-last-word"
    – godlygeek
    Commented Jun 17, 2014 at 17:19
  • 1
    @shadyabhi ^ Links for both bash and zsh's standard keybindings. Note that bash is just using readline, so (most of) the bash ones will work in any application that uses the readline library.
    – godlygeek
    Commented Jun 17, 2014 at 17:20
4

This answer is similar to a couple of the others:

stat filename  &&  file !#:1

Unlike the other answers, which automatically repeat the last argument from the previous command, this one requires you to count:

ls -ld filename  &&  file !#:2

Unlike some of the other answers, this does not require quotes:

stat "useless cat"  &&  file !#:1

or

echo Sometimes a '$cigar' is just a !#:3.
3

The way I have found to do this reasonably simply is using xargs, it takes a file / pipe and converts its contents into a program arguments . This can be combined with tee which splits a stream and sends it to two or more programs, In your case you need:

echo filename | tee >(xargs stat) >(xargs file) | cat

Unlike many of the other answers this will work in bash and most other shells under Linux. I shall suggest this is a good use case of a variable but if you absolutely cannot use one this is the best way to do it only with pipes and simple utilities (assuming you do not have a particularly fancy shell).

Additionally you can do this for any number of programs by simply adding them in the same manner as these two after the tee.

EDIT

As suggested in the comments there are a couple of flaws in this answer, the first is the potential for output interlacing. This can be fixed as so:

echo filename | tee >(xargs stat) >(( wait $!; xargs file )) | cat

This will force the commands to run in turn and output will never be interlaced.

The second issue is avoiding process substitution which is not available in some shells, this can be achieved by using tpipe instead of tee as so:

echo filename | tpipe 'xargs stat' | ( wait $!; xargs file )

This should be portable to nearly any shell (I hope) and solves the issues in the other commend, however I am writing this last one from memory as my current system does not have tpipe available.

2
  • 1
    Process substitution is a ksh feature that only works in ksh (not the public domain variants), bash and zsh. Note that stat and file will run concurrently, so possibly their output will be intertwined (I suppose you're using | cat to force output buffering to limit that effect?). Commented Jun 11, 2014 at 17:40
  • @StéphaneChazelas You are correct about cat, when using it I have not managed to ever produce a case of interlaced input when using it. However I am going to try something to produce a more portable solution momentarily.
    – Vality
    Commented Jun 11, 2014 at 20:52
2

This is similar to a couple of the other answers, but is more portable than this one and much simpler than this one:

sh -c 'stat "$0"; file "$0"' filename

or

sh -c 'stat "$1"; file "$1"' sh filename

in which sh is assigned to $0.  Although it is three characters more to type, this (second) form is preferable because that $0 is the name you give to that inline script.  It's used, for instance, in error messages by the shell for that script, like when file or stat can't be found or can't be executed for any reason.


If you want to do

stat filename1 filename2 filename3 …; file filename1 filename2 filename3

for an arbitrary number of files, do

sh -c 'stat "$@"; file "$@"' sh filename1 filename2 filename3

which works because "$@" is equivalent to "$1" "$2" "$3" ….

1
  • That $0 is the name you want to give to that inline script. It's used for instance in error messages by the shell for that script. Like when file or stat can't be found or can't be executed for any reason. So, you want something meaningful here. If not inspired, just use sh. Commented Jun 20, 2015 at 18:13
1

I think you are asking a wrong question, at least for what you are trying to do. stat and file commands are taking parameters which is the name of a file and not the content of it. It is later on the command which does reading of the file identified by the name you are specifying.

A pipe is supposed to have two ends, one used as input and another one as output and this make sense as you might need to do different processing along the way with different tools until you get needed result. Saying that you know how to pipe outputs but don't know how to pipe inputs is not correct in principle.

In your case you don't need a pipe at all since you have to provide the input in a form of a file name. And even if those tools (stat and file) would read stdin, pipe is not relevant here again as input should not be altered by any of the tool when it gets to the second one.

1

There are a few different references to 'input' here, so I will give a few scenarios with understanding it in mind first. For your quick answer to the question in shortest form:

stat testfile < <($1)> outputfile

The above will perform a stat on testfile, take (redirect) it's STDOUT and include that into the next special function (the <() part) then output the final results of whatever that was, into a new file (outputfile). The file is called, then referenced with bash built-ins ($1 each time after, until you begin a new set of instructions).

Your question is great, and there are several answers and ways to do this, but it does indeed change with what you are doing specifically.

For instance, you can loop that as well, which is quite handy. A common usage of this is, in psuedo-code mindset, is:

run program < <($output_from_program)> my_own.log

Taking that in and expanding on that knowledge allows you to create things such as:

ls -A; (while read line; do printf "\e[1;31mFound a file\e[0m: $line\n"; done) < <(/bin/grep thatword * | /usr/bin/tee -a files_that_matched_thatword)

This will perform a simple ls -A on your current directory, then tell while to loop through each result from the ls -A to (and here's where it's tricky!) grep "thatword" in each of those results, and only perform the previous printf (in red) if it actually found a file with "thatword" in it. It will also log the results of the grep into a new text file, files_that_matched_thatword.

Example output:

ls -A; (while read line; do printf "\e[1;31mFound a file\e[0m: $line\n"; done) < <(/bin/grep thatword * | /usr/bin/tee -a files_that_matched_thatword)

index.html

All of that simply printed the ls -A result, nothing special. Add something for it to grep this time:

echo "thatword" >> newfile

Now re-run it:

ls -A; (while read line; do printf "\e[1;31mFound a file\e[0m: $line\n"; done) < <(/bin/grep thatword * | /usr/bin/tee -a files_that_matched_thatword)

files_that_matched_thatword  index.html  newfile

Found a file: newfile:thatword

While perhaps a more exhausting answer than you're looking for at present, I believe keeping handy notes like this around will benefit you much more in future endeavours.

0

This should work for all file names in the current directory.

sh -c "$(printf 'stat -- "$1";file -- "$1";shift%.0b\n' *)" -- *
12
  • 1
    Assuming the file names don't contain double quote, backslash, dollar, backtick, }... characters and don't start with -. Some printf implementations (zsh, bash, ksh93) have a %q. With zsh, that could be: printf 'stat %q; file %1$q\n' ./* | zsh Commented Jun 11, 2014 at 21:06
  • @StephaneChezales - all of that is fixed now except for the possibility of the hardquote. I could handile that with a single sed s/// I guess, but its about of scope - and if people are putting that in their filenames they should be using windows.
    – mikeserv
    Commented Jun 11, 2014 at 21:21
  • @StéphaneChazelas - nevermind - I forgot how easy those were to handle.
    – mikeserv
    Commented Jun 11, 2014 at 21:27
  • Strictly speaking, this is not an answer to the question as asked: “How can you execute both commands on the same input (without typing the input … twice)?” (emphasis added). Your answer applies to all files in the current directory – and it includes * twice. You might as well say stat -- *; file -- *. Commented Jul 24, 2014 at 20:17
  • @Scott - the input is not typed twice - * is expanded. The expansion of * plus the two commands for every argument in * is the input. See? printf commands\ %s\;shift *
    – mikeserv
    Commented Jul 24, 2014 at 20:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .