207

Let's say that during your workday you repeatedly encounter the following form of columnized output from some command in bash (in my case from executing svn st in my Rails working directory):

?       changes.patch
M       app/models/superman.rb
A       app/models/superwoman.rb

in order to work with the output of your command - in this case the filenames - some sort of parsing is required so that the second column can be used as input for the next command.

What I've been doing is to use awk to get at the second column, e.g. when I want to remove all files (not that that's a typical usecase :), I would do:

svn st | awk '{print $2}' | xargs rm

Since I type this a lot, a natural question is: is there a shorter (thus cooler) way of accomplishing this in bash?


What I am asking is essentially a shell command question even though my concrete example is on my svn workflow.

E_net4
  • 27,810
  • 13
  • 101
  • 139
Sv1
  • 2,414
  • 2
  • 18
  • 13
  • 1
    When you use a command often you are better creating a script and put it in your path. You can simply create a function in your bashrc if you better like. I dont see the point of reducing the column selection expression. – Lynch Sep 06 '11 at 06:22
  • 1
    You are right, and I might do that. The 'point' is the quest for new ways to do stuff in bash, for the purposes of learning but mostly for fun :) – Sv1 Sep 06 '11 at 06:30
  • 1
    Also you don't have your .bashrc when ssh-ing somewhere, so it's useful to know your way around without it. – Kos Oct 07 '14 at 10:07

8 Answers8

149

You can use cut to access the second field:

cut -f2

Edit: Sorry, didn't realise that SVN doesn't use tabs in its output, so that's a bit useless. You can tailor cut to the output but it's a bit fragile - something like cut -c 10- would work, but the exact value will depend on your setup.

Another option is something like: sed 's/.\s\+//'

porges
  • 30,133
  • 4
  • 83
  • 114
130

To accomplish the same thing as:

svn st | awk '{print $2}' | xargs rm

using only bash you can use:

svn st | while read a b; do rm "$b"; done

Granted, it's not shorter, but it's a bit more efficient and it handles whitespace in your filenames correctly.

Rick Sladkey
  • 33,988
  • 6
  • 71
  • 95
  • What is `a` and `b`, how get them? – Timo Nov 24 '17 at 07:11
  • 2
    @Timo `a` represents the first column and `b` represents the remaining columns. If you want to print the second column, use `read a b c;` and then use `echo` instead of `rm`. I used this to get a bunch of process of IDs that followed a grep pattern, so I could interrupt them all. – Matt Kleinsmith Mar 02 '18 at 20:46
  • Actually `while read` loops in the shell tend to be significantly *slower* than using an external utility. See e.g. https://stackoverflow.com/questions/13762625/bash-while-read-loop-extremely-slow-compared-to-cat-why – tripleee Jun 08 '21 at 16:21
56

I found myself in the same situation and ended up adding these aliases to my .profile file:

alias c1="awk '{print \$1}'"
alias c2="awk '{print \$2}'"
alias c3="awk '{print \$3}'"
alias c4="awk '{print \$4}'"
alias c5="awk '{print \$5}'"
alias c6="awk '{print \$6}'"
alias c7="awk '{print \$7}'"
alias c8="awk '{print \$8}'"
alias c9="awk '{print \$9}'"

Which allows me to write things like this:

svn st | c2 | xargs rm
StackedCrooked
  • 34,653
  • 44
  • 154
  • 278
24

Try the zsh. It supports suffix alias, so you can define X in your .zshrc to be

alias -g X="| cut -d' ' -f2"

then you can do:

cat file X

You can take it one step further and define it for the nth column:

alias -g X2="| cut -d' ' -f2"
alias -g X1="| cut -d' ' -f1"
alias -g X3="| cut -d' ' -f3"

which will output the nth column of file "file". You can do this for grep output or less output, too. This is very handy and a killer feature of the zsh.

You can go one step further and define D to be:

alias -g D="|xargs rm"

Now you can type:

cat file X1 D

to delete all files mentioned in the first column of file "file".

If you know the bash, the zsh is not much of a change except for some new features.

HTH Chris

Clauz
  • 107
  • 7
Chris
  • 2,987
  • 2
  • 20
  • 21
  • ahh, I see you've updated your answer while I was typing my comment above :) Can you somehow dynamically specify which column to get, or would I need n-lines in my .zshrc (not that that's important, just curious) – Sv1 Sep 06 '11 at 06:35
  • I have edited my post further and defined a suffix "D" to delete files. As far as i know you will have to add a line for each suffix. – Chris Sep 06 '11 at 06:45
  • Or make it the first parameter of the X command. None of this requires zsh or aliases, except perhaps for the peculiar idea to put a pipe in an alias. – tripleee Sep 06 '11 at 06:58
  • 1
    I dont understand why you all seems to like the `cut` option. It simply does not work on my machine using the output of `svn st`. Try `svn st | cut -d' ' -f2` just to see what happen. – Lynch Sep 06 '11 at 07:08
  • @Sv1 you could write a loop for defining the aliases, since alias is just a command. – floer32 Nov 21 '13 at 15:29
  • Or same thing using awk: `alias -g c1="| awk '{print \$1}'"` – Darkside Oct 13 '14 at 20:54
8

Because you seem to be unfamiliar with scripts, here is an example.

#!/bin/sh
# usage: svn st | x 2 | xargs rm
col=$1
shift
awk -v col="$col" '{print $col}' "${@--}"

If you save this in ~/bin/x and make sure ~/bin is in your PATH (now that is something you can and should put in your .bashrc) you have the shortest possible command for generally extracting column n; x n.

The script should do proper error checking and bail if invoked with a non-numeric argument or the incorrect number of arguments, etc; but expanding on this bare-bones essential version will be in unit 102.

Maybe you will want to extend the script to allow a different column delimiter. Awk by default parses input into fields on whitespace; to use a different delimiter, use -F ':' where : is the new delimiter. Implementing this as an option to the script makes it slightly longer, so I'm leaving that as an exercise for the reader.


Usage

Given a file file:

1 2 3
4 5 6

You can either pass it via stdin (using a useless cat merely as a placeholder for something more useful);

$ cat file | sh script.sh 2
2
5

Or provide it as an argument to the script:

$ sh script.sh 2 file
2
5

Here, sh script.sh is assuming that the script is saved as script.sh in the current directory; if you save it with a more useful name somewhere in your PATH and mark it executable, as in the instructions above, obviously use the useful name instead (and no sh). In the example above, I called it x just to make the shortest possible solution.

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • Nice idea, but does not quite work for me as is on sh or bash. This works: #!/bin/bash col=$1 shift awk "{print \$$col}" – mgk Jan 25 '15 at 14:47
  • Thanks for this late comment. Updated with your fix. – tripleee Jan 25 '15 at 14:53
  • 1
    better `awk -v col=$col ...` – fedorqui Dec 01 '15 at 13:44
  • @fedorqui Thanks for your feedback, here and elsewhere! Updated the answer. It's now slightly longer so if you want the absolutely shortest script you can have, see the [revision history](http://stackoverflow.com/posts/7316134/revisions) for the original version. – tripleee Dec 02 '15 at 05:26
  • Thing here is that I don't understand why you use `shift`. If you run the script like `./script.sh 2`, col will contain `2` so that `awk -v col="$col" '{print $col}'` will print the second column. You also need to mention the file you are using, unless this comes from stdout. – fedorqui Dec 02 '15 at 11:17
  • @fedorqui Gotcha -- added `"${@--}"` to accept a set of input files, else read stdin. "Enough rope to shoot yourself in the foot"... – tripleee Dec 02 '15 at 11:29
  • 1
    @fedorqui Thanks a bunch! Added a small caveat to the `cat` example for hysterical reasons (-: – tripleee Dec 02 '15 at 11:51
  • This is a great solution, vs. an alias. Portable, pipe-able, unix-y – shender May 02 '19 at 03:33
  • If you are really hellbent on finding absolutely the shortest possible solution, you could `alias 1='x 1'`, `alias 2='x 2'` etc. Then you can simply say `1 filename` to extract the first column from `filename`. – tripleee Aug 08 '20 at 11:04
6

It looks like you already have a solution. To make things easier, why not just put your command in a bash script (with a short name) and just run that instead of typing out that 'long' command every time?

Tarek Fadel
  • 1,909
  • 1
  • 14
  • 22
  • Its just that its not as 'cool' in my opinion. Why? Well I don't want to have to flood my .bashrc with various shortcuts that do this and that, essentially writing a meta-shell. Its pretty aliased up as it is. Having said that, your suggestion is not a bad one. – Sv1 Sep 06 '11 at 06:39
  • 2
    `.bashrc`? Why would you clutter it up with non-bash related stuff. What I do is I write individual scripts for each 'set' of commands and put them all in a `~/scripts` directory. Then add the entire `~/scripts` to my `PATH` so I can just call them by name when I need them. – Tarek Fadel Sep 06 '11 at 06:58
  • 4
    Then don't put it in your .bashrc. Create a script in ~/bin and make sure it's on your PATH. – tripleee Sep 06 '11 at 07:00
2

If you are ok with manually selecting the column, you could be very fast using pick:

svn st | pick | xargs rm

Just go to any cell of the 2nd column, press c and then hit enter

  • I tried out the "pick" tool that you referenced, and I like it a lot. It's very good for a lot of situations where I want to print selected columns but don't want to type out "awk '{print $3}'" just to get the desired column. Unfortunately the name conflicts with a different "pick" tool which appears to be more popular -- it can be installed with "apt install pick". So I renamed your tool to "pickk" on my system, and intend to keep using it. Thanks for posting a reference. – William Dye Dec 05 '18 at 17:43
1

Note, that file path does not have to be in second column of svn st output. For example if you modify file, and modify it's property, it will be 3rd column.

See possible output examples in:

svn help st

Example output:

 M     wc/bar.c
A  +   wc/qax.c

I suggest to cut first 8 characters by:

svn st | cut -c8- | while read FILE; do echo whatever with "$FILE"; done

If you want to be 100% sure, and deal with fancy filenames with white space at the end for example, you need to parse xml output:

svn st --xml | grep -o 'path=".*"' | sed 's/^path="//; s/"$//'

Of course you may want to use some real XML parser instead of grep/sed.

Michał Šrajer
  • 30,364
  • 7
  • 62
  • 85