4

I need to split the output from ps, which is spaced separated.

#!/bin/bash

A=$(ps -r -e -o pcpu=,comm= | head -1)
B=${A[0]}
C=${A[1]}
printf '%3s  %s\n' $B $(basename $C)

Output should be:

 42 bar

Instead, I get:

usage: basename ...

Why doesn't this work, and most importantly, how do I make it work?

5

Others have already noted what the error in your code is and correctly suggested that for your initial placeholder data, an array would be the better choice of data structure, along with how to make sure you split the string correctly etc.

Now that we know what your actual command is that you're parsing, we can be slightly more creative with suggestions for improvement.

The following script will take each of the lines of output of your ps command and read it as two space-delimited bits. The body of the loop output the read bits in different ways:

#!/bin/bash

ps -r -e -o pcpu=,comm= |
while IFS=' ' read -r pcpu comm; do
    printf 'pcpu=%s,\tcomm=%s,\tbasename of comm=%s\n' \
        "$pcpu" "$comm" "${comm##*/}"
done

Here, comm will hold everything after the first sequence of spaces in the output of ps (the initial spaces, before the first column, would be trimmed off).

You may obviously insert your head -n 1 as a part of the initial pipeline if you wish.

Note that in some shells, including bash, the loop is running in a subshell, so any variables created there will not be available after the pipeline has finished. There are two solutions to this in bash:

  1. Enable the lastpipe shell option in the script with shopt -s lastpipe, or
  2. Read the data into the loop with a process substitution:

    while IFS=' ' read ...
       # ...
    done < <( ps ... )
    

Example run:

$ bash script.sh
pcpu=0.0,       comm=tmux,      basename of comm=tmux
pcpu=0.0,       comm=sh,        basename of comm=sh
pcpu=0.0,       comm=sh,        basename of comm=sh
pcpu=0.0,       comm=bash,      basename of comm=bash
pcpu=0.0,       comm=bash,      basename of comm=bash
pcpu=0.0,       comm=bash,      basename of comm=bash
pcpu=0.0,       comm=ps,        basename of comm=ps
pcpu=0.0,       comm=sh,        basename of comm=sh
  • It's a fine line knowing how much detail to include, but your suggestion is brilliant, because you guessed the purpose and put forth a very short and UNIX-y solution. I would give two points if I could! – forthrin Aug 6 '18 at 10:22
  • 2
    @forthrin Whenever I see anyone storing the output of a command into a variable, I'd like to know what the actual command is. Chances are that there's a better way of processing that data than to store it in a variable, especially if the command outputs more than a short simple string that requires no further processing. – Kusalananda Aug 6 '18 at 10:33
6

To split a string on space character, you can use the split+glob operator. That's invoked upon command substitution or parameter expansion, but obviously not when storing into a scalar variable which can only store at most one value.

 var=$(...)

Is a scalar variable assignment. So would assign the whole output to $var, same as ${var[0]}. For an array variable assignment, it's:

 var=($(...))

Now, before invoking it, as always, you'd need to tune it:

 set -o noglob # disable the glob part which you don't want here
 IFS=' ' # split on space only
 var=($(some command)) # invoke it

Note that it splits on sequences of spaces and leading and trailing spaces are ignored.

Then, you don't want to invoke it on the expansion of $B nor $C nor $(basename...) so you need to quote those:

 printf '%3s  %s\n' "$B" "$(basename -- "$C")"

Also don't forget the -- to mark the end of options.

Because you forgot the quotes around $C ($C being empty in your case as ${A[1]} was never assigned any value), basename didn't get passed any argument instead of being passed that empty argument and complained about it.

3

define array using (). If you using double quotes, then it will be taking as whole string.

Read more about the array

$ cat test.sh
#!/bin/bash

A=(42 /foo/bar)
B=${A[0]}
C=${A[1]}
printf '%3s  %s\n' $B $(basename $C)

$ bash test.sh
 42  bar
  • "42 /foo/bar" is actually the output of another command. So then ($(echo 42 /foo/bar)) will work, then! Update the answer and I'll accept it. Good answer with simple explanation. – forthrin Aug 6 '18 at 8:15
  • @forthrin What is the other command? – Kusalananda Aug 6 '18 at 8:23
  • @forthrin, doing ($(echo 42 /foo/bar)) would invoke the split+glob operator (actually Kamaraj also invoked 3 times by mistake in their answer (in the part copied from your question)), so it would only work properly if $IFS contained space (and no other character, assuming you want to split on space only) and the string didn't contain wildcard characters. – Stéphane Chazelas Aug 6 '18 at 8:25
0

Another approach using awk:

 echo "42 /foo/bar" | awk '{n=split($2,b,"/"); print $1,b[n]}'
  • 1
    The first explicit split() is not necessary as awk already has split the line into $1 and $2. – Kusalananda Aug 6 '18 at 9:37
  • @Kusalananda noted and answer updated +1 – thatgeeman Aug 6 '18 at 9:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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