6

I am trying to figure out a solution for this question. My approach to this problem so far is as below.

  • Append all the characters together to make it a long string.
  • After the above step, remove all the white spaces or tab spaces so that we will just have one big string.

I was able to establish the above steps with the below command.

column -s '\t' inputfile | tr -d '[:space:]'

So for an input file like this,

1   0   0   0   0   0

0   1   1   1   0   0

After applying the above command I have the values as,

100000011100

Now in this big string I am trying to apply an approach as below.

Extract every 6th character (as the original OP wants), and append it to an array element till the end of the string.

So basically, with the above step, I am trying to create the array elements as,

10 (1st and 7th character), 01 (2nd and 8th character), 01 (3rd and 9th character), 01 (4th and 10th character), 00 (5th and 11th character), 00 (6th and 12th character) .

So my question is, how could I extract every nth character so that I could add them to an array to proceed further? (n=6, in this case).

4 Answers 4

5

Two lines

Here is a pure-bash solution that produces a bash array:

s="100000011100"
array=($(
    for ((i=0; i<${#s}-6; i++))
    do
        echo "${s:$i:1}${s:$((i+6)):1}"
    done
    ))
echo "${array[@]}"

This produces the same output as shown in the question:

10 01 01 01 00 00

The key element here is the use of bash's substring expansion. Bash allows the extraction substrings from a variable, say parameter, via ${parameter:offset:length}. In our case, the offset is determined by the loop variable i and the length is always 1.

General Solution For Any Number of Lines

Suppose, for example, that our original string has 18 characters and we want to extract the i-th, the i+6-th, and the i+12-th characters for i from 0 to 5. Then:

s="100000011100234567"
array=($(
    for ((i=0; i<6; i++))
    do
        new=${s:$i:1}
        for ((j=i+6; j<${#s}; j=j+6))
        do 
            new="$new${s:$j:1}"
        done
        echo "$new"
    done
    ))

echo "${array[@]}"

This produces the output:

102 013 014 015 006 007

This same code extends to an arbitrary number of 6-character lines. For example, if s has three lines (18 characters):

s="100000011100234567abcdef"

Then, the output becomes:

102a 013b 014c 015d 006e 007f
5
  • thanks. I have the same problem as I have mentioned in Gnouc's answer. If I have 3 lines, it seems to break.
    – Ramesh
    Oct 14, 2014 at 4:59
  • 1
    @Ramesh See update.
    – John1024
    Oct 14, 2014 at 5:12
  • cool. This one also works as expected. Thanks again for providing a bash solution.
    – Ramesh
    Oct 14, 2014 at 5:16
  • I have used your solution to address the original problem of the OP in the question I linked. Thanks again.
    – Ramesh
    Oct 14, 2014 at 6:04
  • @Ramesh So, this is combined with wc -w. Nice. +1 for your answer to the other question.
    – John1024
    Oct 14, 2014 at 6:09
4

Using perl:

$ echo 100000011100 | perl -nle '
    for ($i = 0; $i < length()/2; $i++) {
        print substr($_,$i,1), substr($_,$i+6,1);
    }
'
10
01
01
01
00
00

It works for two lines. If you want to work with arbitrary of lines, you should process lines directly, instead of building big string. With this input:

1   0   0   0   0   0                                                           
0   1   1   1   0   0                                                           
0   0   0   0   0   0

Try:

$ perl -anle '
    for ($i = 0; $i <= $#F; $i++) {
      push @{$h{$i}}, $F[$i];
    }
    END {
        print @{$h{$_}} for keys %h;
    }
' file
000
010
000
100
010
010
3
  • Thanks. But if I have 3 lines, I want to get the substrings as 101, 001 and so on. But I am getting only 2 characters with this approach.
    – Ramesh
    Oct 14, 2014 at 4:41
  • 1
    @Ramesh: See my updated.
    – cuonglm
    Oct 14, 2014 at 5:03
  • cool. This one works. You should probably add this one as the answer to the other question.
    – Ramesh
    Oct 14, 2014 at 5:06
3

As a shell solution, getopts is probably easiest. The thing about getopts is that it is POSIX-specified to do exactly what you're asking - process a byte-stream in a shell-loop. I know that sounds weird, because, if you're like me before I learned this, you're probably thinking, well, gee, I thought it was supposed to handle command-line switches. Which is true, but so is the first thing. Consider:

-thisisonelongstringconsistingofseparatecommandlineswitches

Yes, getopts has to handle that. It has to split that char by char in a loop and return to you each character in either the shell variable $OPTARG or in another that you specify by name all depending on how specific you get when you call it. What's more, it has to return errors in shell variables and save its progress when it does in the shell variable $OPTIND so it can resume right where it left off if you can somehow address it. And it has to do the whole job without invoking a single subshell.

So let's say we have:

arg=$(seq -s '' 1000); set --
while getopts :0123456789 v -"${arg}"
do [ "$((i=$i+1<6?$i+1:0))" -gt 0 ] ||
set "$@" "$v"
done

Hmmm.... I wonder if it worked?

echo "$((${#arg}/6))" "$#"
482 482

That's nice...

eval '
printf %.1s\\n "${arg#'"$(printf %0$((124*6-1))d | tr 0 \?)"'}" "${124}"'
4
4

So, as you can see, the getopts command completely set the array for every sixth byte in the string. And it doesn't have to be numbers like this - nor must it even be shell safe characters - and you needn't even specify the target chars as I did above with 01234565789 either. I've tested this repeatedly in a lot of shells and they all just work. There are some quirks - bash will throw away the first character if it is a whitespace character - dash accepts the : colon as a specified parameter even though it is just about the only POSIX specifically forbids. But none of that matters because getopts still deposits the current opt char's value in $OPTARG even when it returns you an error (represented by a ? assigned to your specified opt var) and otherwise explictly unsets $OPTARG unless you've declared an option should have an argument. And the whitespace thing is kind of a good thing - it only discards a leading space, which is excellent, because, when working with unknown values, you can do:

getopts : o -" $unknown_value"

...to kick off the loop without any danger of the first character actually being in your accepted args string - which would result in getopts sticking the whole thing in $OPTARG at once - as an argument.

Here's another example:

OPTIND=1
while getopts : o -" $(dd if=/dev/urandom bs=16 count=1 2>/dev/null)"                         
do printf '\\%04o' "'$OPTARG"; done  

\0040\0150\0071\0365\0320\0070\0161\0064\0274\0115\0012\0215\0222\0271\0146\0057\0166

I set $OPTIND=1 in the first line because I just used getopts and, until you reset it, it expects its next call to continue where it left off - it wants "${arg2}" in other words. But I don't feel like giving and I'm doing a different thing now, so I let it know by resetting $OPTIND at which point it's good to go.

In this one I used zsh - which doesn't quibble about a leading space - and so the first character is octal 40 - the space character. I don't usually use getopts in that way, though - I usually use it to avoid doing a write() for each byte and instead assign its output - which comes in a variable - to another shell variable - as I did above with set after a fashion. Then, when I'm ready I can take the whole string and when I do usually strip the first byte.

1
  • you gotta give a little to get the little. for shame, me.
    – mikeserv
    Jan 12, 2019 at 10:39
1

sed is the first thing that pops into my mind.

$ echo 1234567890abcdefghijklmnopqrstuvwxyz | sed 's/.\{5\}\(.\)/\1/g'
6bhntz

Match 5 characters, capture the 6th, and replace them all with that captured character.

This will however have an issue if the length of the string isn't an exact multiple of 6:

$ echo 1234567890abcdefghijklmnopqrstuvwxy | sed 's/.\{5\}\(.\)/\1/g' 
6bhntuvwxy

But we can fix this by altering the sed a little:

$ echo 1234567890abcdefghijklmnopqrstuvwxy | sed 's/.\{1,5\}\(.\{0,1\}\)/\1/g'
6bhnt

Due to the greedy nature of regex, the variable length matches will match as much as they can, and if there's nothing left for the capture, then it doesn't capture, and the characters are just deleted.

4
  • Thanks. So I will still need a loop to store the sed's output to an array variable?
    – Ramesh
    Oct 14, 2014 at 4:43
  • Not necessarily. read -a foo < <(echo 1234567890ab | sed 's/.\{1,5\}\(.\{0,1\}\)/\1 /g')
    – phemmer
    Oct 14, 2014 at 4:45
  • your solution seems to work even if I have more than 2 lines. But I still get the last line as the array value. Instead I want all the values to be stored in my array. This is the command am trying. read -a array_elements < <(column -s '\t' inputfile | tr -d '[:space:]' | sed 's/.\{5\}\(.\)/\1/g')
    – Ramesh
    Oct 14, 2014 at 5:02
  • Read the rest of the answer. It explains the issue.
    – phemmer
    Oct 14, 2014 at 12:40

You must log in to answer this question.

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