2

Assuming that I have a string like this one:

string="1 0 . @ 1 1 ? 2 2 4"

Is it possible to concatenate digits that are next to each other?

So that string be like: 10 . @ 11 ? 224 ?

I found only basic things how to distinguish integers from other characters and how to "connect" them. But I have no idea how to iterate properly.

num=""
 for char in $string; do
    if [  $char -eq $char 2>/dev/null ] ; then
        num=$num$char
mklement0
  • 382,024
  • 64
  • 607
  • 775
davoid
  • 327
  • 2
  • 10
  • Sorry, my mistake. Edited. – davoid Nov 23 '16 at 15:55
  • Are you allowed to use `bash` shell? – Inian Nov 23 '16 at 15:55
  • 1
    No, I use only POSIX sh. – davoid Nov 23 '16 at 15:56
  • @davoid are you allowed to use external commands such as `sed`, `awk` etc? – riteshtch Nov 23 '16 at 16:04
  • 1
    @Inian connecting digits, not characters. Yes, i can use sed for instance. – davoid Nov 23 '16 at 16:06
  • 1
    One of the things that makes this interesting, btw, is that `for char in $string` will treat `?`s and `*`s in your string as glob characters, thus replacing a `?` with a list of single-character filenames in your current directory, or a `*` with a list of all filenames in the current directory. Thus, you can only use `for` safely in this manner if you first turn off globbing. – Charles Duffy Nov 23 '16 at 16:32
  • 1
    BTW, being able to put `2>/dev/null` anywhere in a command line (as you do here, putting it partway through your `test` command) is a bashism, not guaranteed to work with pure POSIX, which only guarantees that redirections are supported at the very beginning and very end of a command. – Charles Duffy Nov 23 '16 at 16:34

4 Answers4

4

Here's an almost pure-shell implementation -- transforming the string into a character per line and using a BashFAQ #1 while read loop.

string="1 0 . @ 1 1 ? 2 2 4"
output=''

# replace spaces with newlines for easier handling
string=$(printf '%s\n' "$string" | tr ' ' '\n')

last_was_number=0
printf '%s\n' "$string" | {
  while read -r char; do
    if [ "$char" -eq "$char" ] 2>/dev/null; then # it's a number
      if [ "$last_was_number" -eq "1" ]; then
        output="$output$char"
        last_was_number=1
        continue
      fi
      last_was_number=1
    else
      last_was_number=0
    fi
    output="$output $char"
  done
  printf '%s\n' "$output"
}
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
2

To complement Charles Duffy's helpful, POSIX-compliant sh solution with a more concise perl alternative:

Note: perl is not part of POSIX, but it is preinstalled on most modern Unix-like platforms.

$ printf '%s\n' "1 0 . @ 1 1 ? 2 2 4" | perl -pe 's/\d( \d)+/$& =~ s| ||gr/eg' 
10 . @ 11 ? 224
  • The outer substitution, s/\d( \d)+/.../eg, globally (g) finds runs of at least 2 adjacent digits (\d( \d)+), and replaces each run with the result of the expression (e) specified as the replacement string (represented as ... here).

  • The expression in the inner substitution, $& =~ s| ||gr, whose result is used as the replacement string, removes all spaces from each run of adjacent digits:

    • $& represents what the outer regex matched - the run of adjacent digits.
    • =~ applies the s call on the RHS to the LHS, i.e., $& (without this, the s call would implicitly apply to the entire input string, $_).
    • s| ||gr replaces all (g) instances of <space> from the value of the value of $& and returns (r) the result, effectively removing all spaces.
      • Note that | is used arbitrarily as the delimiter character for the s call, so as to avoid a clash with the customary / delimiter used by the outer s call.
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
2

POSIX compliant one-liner with sed:

string="1 0 . @ 1 1 ? 2 2 4"
printf '%s\n' "$string" | sed -e ':b' -e ' s/\([0-9]\) \([0-9]\)/\1\2/g; tb'

It just iteratively removes the any space between two digits until there aren't any more, resulting in:

10 . @ 11 ? 224
that other guy
  • 116,971
  • 11
  • 170
  • 194
0

Here is my solution:

string="1 0 . @ 1 1 ? 2 2 4"
array=(${string/// })
arraylength=${#array[@]}
pattern="[0-9]"
i=0

while true; do
    str=""
    start=$i

    if [ $i -eq $arraylength ]; then
        break;
    fi

    for (( j=$start; j<${arraylength}; j++ )) do
       curr=${array[$j]}
       i=$((i + 1))

       if [[ $curr =~ $pattern ]]; then 
           str="$str$curr"
       else
           break
       fi
   done

   echo $str
done
bob_saginowski
  • 1,429
  • 2
  • 20
  • 35
  • 2
    It's not obvious, but the tags and the OP's comments imply that a pure, portable `sh` solution is desired. This is not to say that future readers won't come here looking for `bash` solutions, but please make it clear that your answer requires `bash`. `${string/// }` has no effect on the sample input. Initializing an array with an unquoted parameter expansion such as `( ${string} )` not only splits the string into words, but also applies _globbing_ to the resulting words; if the current dir. has single-char filenames, `?` expands to them. Consider not mixing `[…]` and `[[…]]` in your code. – mklement0 Nov 23 '16 at 17:47
  • 1
    It would be considerably less buggy (and just as bash-specific) to use `read -r -a array <<<"$string"` to read your string into an array. I would also avoid iterating by index when you have no need to use that index and can just as easily (and more readably) iterate over contents directly. – Charles Duffy Nov 23 '16 at 20:25
  • 1
    ...that said, if this were being written for bash, I would be going a completely different route. I mean, you've got `BASH_REMATCH`, so you can access match groups for your regexes -- this makes iterating over places where you have `[[:digit:]][[:space:]][[:digit:]]` and removing the space outright trivial, without any need for an array or a `for` loop at all. – Charles Duffy Nov 23 '16 at 20:27