Plain Bytes
If the string contains only ASCII bytes, and no new lines, you could use cut
.
The command cut
could work with characters only bytes.
$ echo "ajgjkggéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð" | cut -b 1-5
ajgjk
But that will fail as soon as the cut string contains multi-byte characters:
$ echo "ajgjkggéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð" | cut -b 7-12
géós
There are 6 bytes (7,8,9,10,11, and 12) but those are not 6 characters.
And cut also breaks if there are new lines in the string.
Characters
To work with "multi-byte" characters, we need a tool that understand such multi-byte characters, both sed
and awk
do.
The command sed
could extract characters from an string:
$ s=5;l=3;echo "ajgjkggéóskm" | sed -E 's/^.{'"$s"'}(.{'"$l"'}).*/\1/'
ggé
But the -E option is a GNU extension, so we need to change the line to make it compatible with a POSIX sed (only BRE regex):
$ s=5;l=3;echo "ajgjkggéóskm" | sed 's/^.\{'"$s"'\}\(.\{'"$l"'\}\).*/\1/'
ggé
If the string does not contain new-lines. As sed breaks the input is lines at each new-line character.
That breaks as soon as there are new lines:
$ s=1;l=3;echo $'ajéw\nóskmæß\nðqwee' | sed 's/^.\{'"$s"'\}\(.\{'"$l"'\}\).*/\1/'
jéw
skm
qwe
The output is indeed 3 characters starting with the first (1), but for each line.
Chars and New-Lines.
The only other tool available is awk. Which does have a POSIX specification.
Using the available String Functions from AWK:
$ s=6;l=4;echo "ajgjkggéóskm" | awk -v m="$s" -v n="$l" '{print substr($0,m,n)}'
ggéó
But that also breaks on new-lines:
$ s=1;l=3;echo $'ajéw\nóskmæß\nðqwee' | awk -v m="$s" -v n="$l" '{print substr($0,m,n)}'
ajé
ósk
ðqw
However, we can specify one character that should not be in use in the string, as the RS (record separator): the null byte (\0). That forbids strings that contain NULs (\0), a very very rare problem.
Please note that I am not talking about the empty string: ''
, as that will make awk use an "empty line" as the record separator.
To do that, I will use a capacity of bash (not all shells could do this) of writing a zero byte as this: $'\0'
. For other shells, the solution must be different.
If AWK is set with that RS, it will get the whole input as one record.
$ s=1;l=3;echo $'ajéw\nóskmæß\nðqwee' |
awk -v RS=$'\0' -v m="$s" -v n="$l" '{print substr($0,m,n)}'
ajé
No more new-line interference. Well, we need to use printf to avoid some issues with print an new lines. With that, we could build an script.
Some notes on the script, as is not so standard:
- The script starts execution on the last line:
main "$@"
. That ensures that the whole script has been read by bash and that both defined function have been parsed.
- The line(s) between the two
_safe_place_for_string_
should be filled with any text that you need to include with the file.
- The last
_safe_place_for_string_
should be at the start of a line, have no additional text after it (not even spaces), and start at the start of the line (or after a tab character).
- After that, there must be the close of the function
}
and the start of execution: main "$@"
The script:
#!/bin/bash
main(){
while [ $# -gt 0 ]; do
s=${1//[^0-9]/}; s="$((${s?Missing start of text.}+0))"
l=${2//[^0-9]/}; s="$((${l?Missing start of text.}+0))"
echo "from $s read $l characters"
shift 2
readchars "$s" "$l"
done
}
readchars(){
awk -v RS=$'\0' -v sstr="$1" -v lstr="$2" '
{printf("%s\n",substr($0,sstr,lstr))}
' <<-\_safe_place_for_string_
aéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð
aéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð
aéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð
_safe_place_for_string_
}
main "$@"
Call the script as:
$ ./script.sh 35 12
from 35 read 12 characters
mæßð
aéóskmæ
Note that the first "new-line" comes from inside the string. The last "new-line" was added by the \n
in the printf, you could remove it, if needed.
Or even as ./script.sh 35 12 17 12
, the internal while
will process the repeated calls.
Remember to place your text in the script to get the output you expect.
awk -v l="$1" '{print substr($0, 0, l)}' <file