Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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?

share|improve this question
13  
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 Jun 11 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 Jun 12 at 10:18
1  
And the difference is that < and > are redirection, not piping. A pipeline connects two processes, while redirection simply reassigns stdin/stdout. –  bdowning Jun 12 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. ;-) –  James Haigh Jun 13 at 15:43

10 Answers 10

up vote 12 down vote accepted

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.

share|improve this answer
    
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). –  Stéphane Chazelas Jun 11 at 17:35
    
@StéphaneChazelas: Oh, yes, my mistake, I deleted it. –  cuonglm Jun 11 at 17:37

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;"
share|improve this answer
6  
Sorry, but I cannot resist. –  Andreas Wiese Jun 11 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. –  Stéphane Chazelas Jun 11 at 14:54
    
Too bad you lose the ability to "tab out" (auto-complete) the fileName, due to the quote. –  Lonniebiz Jun 27 at 18:35

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.

share|improve this answer
2  
{ 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 –  1_CR Jun 11 at 16:22
1  
@1_CR, yes. stat - tells stat to do an fstat() on its fd 0. –  Stéphane Chazelas Jun 11 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 Jun 11 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...) –  mr.spuratic Jun 12 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. –  Stéphane Chazelas Jun 12 at 11:43

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.

share|improve this answer
    
what other options are available in zsh/bash that are like Esc _ ? Can you please link with the official documentation? –  shadyabhi Jun 17 at 3:12
1  
Bash: gnu.org/software/bash/manual/bashref.html#Command-Line-Editing and search for "M-_" –  godlygeek Jun 17 at 17:17
1  
zsh: linux.die.net/man/1/zshzle and search for "insert-last-word" –  godlygeek Jun 17 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 Jun 17 at 17:20
    
Thanks @godlygeek. –  shadyabhi Jun 18 at 6:49

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.

share|improve this answer
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?). –  Stéphane Chazelas Jun 11 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 Jun 11 at 20:52

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.
share|improve this answer

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.

share|improve this answer

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

sh -c "$(printf 'stat -- "$1";file -- "$1";shift%.0b\n' *)" -- *
share|improve this answer
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 –  Stéphane Chazelas Jun 11 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 Jun 11 at 21:21
    
@StéphaneChazelas - nevermind - I forgot how easy those were to handle. –  mikeserv Jun 11 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 -- *. –  Scott Jul 24 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 Jul 24 at 20:27

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.

share|improve this answer

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

If you want to do

stat filename1 filename2 filename3 …; file filename1 filename2 filename3

for an arbitrary number of files, do

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

where QuickBrownFoxJumpsOverTheLazyDog is a placeholder, necessary because "$@" is equivalent to "$1" "$2" "$3" …, so you need to interpose something to be assigned to "$0", which is then never used. The placeholder can be any single word, such as a null string ("") or a hyphen (-).

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.