Padding characters in printf - bash

I am writing a bash shell script to display if a process is running or not.
So far, I got this:
printf "%-50s %s\n" $PROC_NAME [UP]
The code gives me this output:
JBoss [DOWN]
GlassFish [UP]
verylongprocessname [UP]
I want to pad the gap between the two fields with a '-' or '*' to make it more readable. How do I do that without disturbing the alignment of the fields?
The output I want is:
JBoss ------------------------------------------- [DOWN]
GlassFish --------------------------------------- [UP]
verylongprocessname ----------------------------- [UP]

Pure Bash, no external utilities
This demonstration does full justification, but you can just omit subtracting the length of the second string if you want ragged-right lines.
pad=$(printf '%0.1s' "-"{1..60})
padlength=40
string2='bbbbbbb'
for string1 in a aa aaaa aaaaaaaa
do
printf '%s' "$string1"
printf '%*.*s' 0 $((padlength - ${#string1} - ${#string2} )) "$pad"
printf '%s\n' "$string2"
string2=${string2:1}
done
Unfortunately, with that technique, the length of the pad string has to be hardcoded to be longer than the longest one you think you'll need, but the padlength can be a variable as shown. However, you can replace the first line with these three to be able to use a variable for the length of the pad:
padlimit=60
pad=$(printf '%*s' "$padlimit")
pad=${pad// /-}
So the pad (padlimit and padlength) could be based on terminal width ($COLUMNS) or computed from the length of the longest data string.
Output:
a--------------------------------bbbbbbb
aa--------------------------------bbbbbb
aaaa-------------------------------bbbbb
aaaaaaaa----------------------------bbbb
Without subtracting the length of the second string:
a---------------------------------------bbbbbbb
aa--------------------------------------bbbbbb
aaaa------------------------------------bbbbb
aaaaaaaa--------------------------------bbbb
The first line could instead be the equivalent (similar to sprintf):
printf -v pad '%0.1s' "-"{1..60}
Or similarly for the more dynamic technique:
printf -v pad '%*s' "$padlimit"
Or this (which allows multi-character "ellipses" without having to modify the format string to accommodate the number of characters - .1 in the example above). It assumes that variables with names such as $_1, $_2, etc., are unset or empty.:
printf -v pad '%s' "<>"$_{1..60}
You can do the printing all on one line if you prefer:
printf '%s%*.*s%s\n' "$string1" 0 $((padlength - ${#string1} - ${#string2} )) "$pad" "$string2"

Pure Bash. Use the length of the value of 'PROC_NAME' as offset for the fixed string 'line':
line='----------------------------------------'
PROC_NAME='abc'
printf "%s %s [UP]\n" $PROC_NAME "${line:${#PROC_NAME}}"
PROC_NAME='abcdef'
printf "%s %s [UP]\n" $PROC_NAME "${line:${#PROC_NAME}}"
This gives
abc ------------------------------------- [UP]
abcdef ---------------------------------- [UP]

Trivial (but working) solution:
echo -e "---------------------------- [UP]\r$PROC_NAME "

I think this is the simplest solution. Pure shell builtins, no inline math. It borrows from previous answers.
Just substrings and the ${#...} meta-variable.
A="[>---------------------<]";
# Strip excess padding from the right
#
B="A very long header"; echo "${A:0:-${#B}} $B"
B="shrt hdr" ; echo "${A:0:-${#B}} $B"
Produces
[>----- A very long header
[>--------------- shrt hdr
# Strip excess padding from the left
#
B="A very long header"; echo "${A:${#B}} $B"
B="shrt hdr" ; echo "${A:${#B}} $B"
Produces
-----<] A very long header
---------------<] shrt hdr

Simple but it does work:
printf "%-50s%s\n" "$PROC_NAME~" "~[$STATUS]" | tr ' ~' '- '
Example of usage:
while read PROC_NAME STATUS; do
printf "%-50s%s\n" "$PROC_NAME~" "~[$STATUS]" | tr ' ~' '- '
done << EOT
JBoss DOWN
GlassFish UP
VeryLongProcessName UP
EOT
Output to stdout:
JBoss -------------------------------------------- [DOWN]
GlassFish ---------------------------------------- [UP]
VeryLongProcessName ------------------------------ [UP]

There's no way to pad with anything but spaces using printf. You can use sed:
printf "%-50s#%s\n" $PROC_NAME [UP] | sed -e 's/ /-/g' -e 's/#/ /' -e 's/-/ /'

echo -n "$PROC_NAME $(printf '\055%.0s' {1..40})" | head -c 40 ; echo -n " [UP]"
Explanation:
printf '\055%.0s' {1..40} - Create 40 dashes
(dash is interpreted as option so use escaped ascii code instead)
"$PROC_NAME ..." - Concatenate $PROC_NAME and dashes
| head -c 40 - Trim string to first 40 chars

This one is even simpler and execs no external commands.
$ PROC_NAME="JBoss"
$ PROC_STATUS="UP"
$ printf "%-.20s [%s]\n" "${PROC_NAME}................................" "$PROC_STATUS"
JBoss............... [UP]

using echo only
The anwser of #Dennis Williamson is working just fine except I was trying to do this using echo. Echo allows to output charcacters with a certain color. Using printf would remove that coloring and print unreadable characters. Here's the echo-only alternative:
string1=abc
string2=123456
echo -en "$string1 "
for ((i=0; i< (25 - ${#string1}); i++)){ echo -n "-"; }
echo -e " $string2"
output:
abc ---------------------- 123456
of course you can use all the variations proposed by #Dennis Williamson whether you want the right part to be left- or right-aligned (replacing 25 - ${#string1} by 25 - ${#string1} - ${#string2} etc...

Here's another one:
$ { echo JBoss DOWN; echo GlassFish UP; } | while read PROC STATUS; do echo -n "$PROC "; printf "%$((48-${#PROC}))s " | tr ' ' -; echo " [$STATUS]"; done
JBoss -------------------------------------------- [DOWN]
GlassFish ---------------------------------------- [UP]

If you are ending the pad characters at some fixed column number, then you can overpad and cut to length:
# Previously defined:
# PROC_NAME
# PROC_STATUS
PAD="--------------------------------------------------"
LINE=$(printf "%s %s" "$PROC_NAME" "$PAD" | cut -c 1-${#PAD})
printf "%s %s\n" "$LINE" "$PROC_STATUS"

Simple Console Span/Fill/Pad/Padding with automatic scaling/resizing Method and Example.
function create-console-spanner() {
# 1: left-side-text, 2: right-side-text
local spanner="";
eval printf -v spanner \'"%0.1s"\' "-"{1..$[$(tput cols)- 2 - ${#1} - ${#2}]}
printf "%s %s %s" "$1" "$spanner" "$2";
}
Example: create-console-spanner "loading graphics module" "[success]"
Now here is a full-featured-color-character-terminal-suite that does everything in regards to printing a color and style formatted string with a spanner.
# Author: Triston J. Taylor <pc.wiz.tt#gmail.com>
# Date: Friday, October 19th, 2018
# License: OPEN-SOURCE/ANY (NO-PRODUCT-LIABILITY OR WARRANTIES)
# Title: paint.sh
# Description: color character terminal driver/controller/suite
declare -A PAINT=([none]=`tput sgr0` [bold]=`tput bold` [black]=`tput setaf 0` [red]=`tput setaf 1` [green]=`tput setaf 2` [yellow]=`tput setaf 3` [blue]=`tput setaf 4` [magenta]=`tput setaf 5` [cyan]=`tput setaf 6` [white]=`tput setaf 7`);
declare -i PAINT_ACTIVE=1;
function paint-replace() {
local contents=$(cat)
echo "${contents//$1/$2}"
}
source <(cat <<EOF
function paint-activate() {
echo "\$#" | $(for k in ${!PAINT[#]}; do echo -n paint-replace \"\&$k\;\" \"\${PAINT[$k]}\" \|; done) cat;
}
EOF
)
source <(cat <<EOF
function paint-deactivate(){
echo "\$#" | $(for k in ${!PAINT[#]}; do echo -n paint-replace \"\&$k\;\" \"\" \|; done) cat;
}
EOF
)
function paint-get-spanner() {
(( $# == 0 )) && set -- - 0;
declare -i l=$(( `tput cols` - ${2}))
eval printf \'"%0.1s"\' "${1:0:1}"{1..$l}
}
function paint-span() {
local left_format=$1 right_format=$3
local left_length=$(paint-format -l "$left_format") right_length=$(paint-format -l "$right_format")
paint-format "$left_format";
paint-get-spanner "$2" $(( left_length + right_length));
paint-format "$right_format";
}
function paint-format() {
local VAR="" OPTIONS='';
local -i MODE=0 PRINT_FILE=0 PRINT_VAR=1 PRINT_SIZE=2;
while [[ "${1:0:2}" =~ ^-[vl]$ ]]; do
if [[ "$1" == "-v" ]]; then OPTIONS=" -v $2"; MODE=$PRINT_VAR; shift 2; continue; fi;
if [[ "$1" == "-l" ]]; then OPTIONS=" -v VAR"; MODE=$PRINT_SIZE; shift 1; continue; fi;
done;
OPTIONS+=" --"
local format="$1"; shift;
if (( MODE != PRINT_SIZE && PAINT_ACTIVE )); then
format=$(paint-activate "$format&none;")
else
format=$(paint-deactivate "$format")
fi
printf $OPTIONS "${format}" "$#";
(( MODE == PRINT_SIZE )) && printf "%i\n" "${#VAR}" || true;
}
function paint-show-pallette() {
local -i PAINT_ACTIVE=1
paint-format "Normal: &red;red &green;green &blue;blue &magenta;magenta &yellow;yellow &cyan;cyan &white;white &black;black\n";
paint-format " Bold: &bold;&red;red &green;green &blue;blue &magenta;magenta &yellow;yellow &cyan;cyan &white;white &black;black\n";
}
To print a color, that's simple enough: paint-format "&red;This is %s\n" red
And you might want to get bold later on: paint-format "&bold;%s!\n" WOW
The -l option to the paint-format function measures the text so you can do console font metrics operations.
The -v option to the paint-format function works the same as printf but cannot be supplied with -l
Now for the spanning!
paint-span "hello " . " &blue;world" [note: we didn't add newline terminal sequence, but the text fills the terminal, so the next line only appears to be a newline terminal sequence]
and the output of that is:
hello ............................. world

Bash + seq to allow parameter expansion
Similar to #Dennis Williamson answer, but if seq is available, the length of the pad string need not be hardcoded. The following code allows for passing a variable to the script as a positional parameter:
COLUMNS="${COLUMNS:=80}"
padlength="${1:-$COLUMNS}"
pad=$(printf '\x2D%.0s' $(seq "$padlength") )
string2='bbbbbbb'
for string1 in a aa aaaa aaaaaaaa
do
printf '%s' "$string1"
printf '%*.*s' 0 $(("$padlength" - "${#string1}" - "${#string2}" )) "$pad"
printf '%s\n' "$string2"
string2=${string2:1}
done
The ASCII code "2D" is used instead of the character "-" to avoid the shell interpreting it as a command flag. Another option is "3D" to use "=".
In absence of any padlength passed as an argument, the code above defaults to the 80 character standard terminal width.
To take advantage of the the bash shell variable COLUMNS (i.e., the width of the current terminal), the environment variable would need to be available to the script. One way is to source all the environment variables by executing the script preceded by . ("dot" command), like this:
. /path/to/script
or (better) explicitly pass the COLUMNS variable when executing, like this:
/path/to/script $COLUMNS

Related

BASH text edit with seq

With this I can callmyscrip.sh 100 and this will print 100 rows with the content generated by seq, but what's the best way to separate the content TEXT="xxx yyy ${this}" for readability with a variable?
#!/bin/bash
howmanytimes=$1
for this in $(seq -w ${howmanytimes}); do echo " /
-- ${this}
"; done
this instead would not work as $this isn't replaced:
#!/bin/bash
howmanytimes=$1
TEXT="THIS WOULD NOT WORK: ${this}"
for this in $(seq -w ${howmanytimes}); do echo ${TEXT} ; done
export $TEXT
seq(1) is nonstandard, inefficient and useless.
Check http://mywiki.wooledge.org/BashGuide/TestsAndConditionals#Conditional_Loops
With ksh:
#!/bin/ksh
txt='this should work with int: '
for i in {0..$1}; do
echo "$txt $i"
done
With bash:
#!/bin/bash
txt='this should work with int: '
for ((i=0; i<=$1; i++)) {
echo "$txt $i"
}
You can wrap your dynamic text in a bash function:
#!/bin/bash
get_content() {
echo "THIS WOULD WORK: $1"
}
how_many_times=$1
for i in $(seq -w ${how_many_times}); do
echo "$(get_content $i)"
done
If you just need to output the content, can simplify it like this:
#!/bin/bash
get_content() {
echo "THIS WOULD WORK: $1"
}
how_many_times=$1
for i in $(seq -w ${how_many_times}); do
get_content $i
done
Check your script with shellcheck. printf is a simple template language. I could see:
#!/bin/bash
howmanytimes=$1
text="THIS WOULD WORK: %s"
for this in $(seq -w "${howmanytimes}"); do
printf "$text" "$this"
done
You could use envsubst to replace environment, however in this case printf looks way clearer. Research quoting in shell.
#!/bin/bash
howmanytimes=$1
text='THIS WOULD WORK: ${THIS}'
for this in $(seq -w "${howmanytimes}"); do
THIS="$this" envsubst <<<"$text"
done
You can use printf directly, and skip the loop entirely:
#!/bin/bash
howmanytimes=$1
text="This WILL work: %s"
printf "${text}\n" $(seq -w ${howmanytimes})
Note that \n needs to be added to the format string, since printf doesn't add a newline automatically like echo does. If you want additional newlines (like in the example), you can add them as either \n or actual newlines, in either the format variable or where it's used in the printf argument. Also, if you want to include a literal backslash or percent sign in the string, double it (i.e. %% to print %, or \\ to print \).
BTW, since printf is a bash builtin, it's not subject to the normal argument list length limits, so this'll work even with very large numbers of numbers.

Changing alternative character from lower to upper and upper to low - Unix shell script

How to convert the alternative character of a string passed to script, if it is lower then it should be converted to upper and if it is upper then to lower??
read -p " Enter string" str
for i in `seq 0 ${#str}`
do
#echo $i
rem=$(($i % 2 ))
if [ $rem -eq 0 ]
then
echo ${str:$i:1}
else
fr=${str:$i:1}
if [[ "$fr" =~ [A-Z] ]]
then
echo ${str:$i:1} | tr '[:upper:]' '[:lower:]'
elif [[ "$fr" =~ [a-z] ]]
then
echo ${str:$i:1} | tr '[:lower:]' '[:upper:]'
else
echo ""
fi
fi
done
Your question is a bit challenging given that it is tagged shell and not as a question pertaining to an advanced shell like bash or zsh. In POSIX shell, you have no string indexes, no C-style for loop, and no [[ .. ]] operator to use character class pattern matching.
However, with a bit of awkward creativity, the old expr and POSIX string and arithmetic operations, and limiting your character strings to ASCII characters, you can iterate over a string changing uppercase to lowercase and lowercase and uppercase while leaving all other characters unchanged.
I wouldn't recommend the approach if you have an advanced shell available, but if you are limited to POSIX shell, as your question is tagged, it will work, but don't expect it to be super-fast...
#!/bin/sh
a=${1:-"This Is My 10TH String"} ## input and output strings
b=
i=1 ## counter and string length
len=$(expr length "$a")
asciiA=$(printf "%d" "'A") ## ASCII values for A,Z,a,z
asciiZ=$(printf "%d" "'Z")
asciia=$(printf "%d" "'a")
asciiz=$(printf "%d" "'z")
echo "input : $a" ## output original string
while [ "$i" -le "$len" ]; do ## loop over each character
c=$(expr substr "$a" "$i" "1") ## extract char from string
asciic=$(printf "%d" "'$c") ## convert to ASCII value
## check if asciic is [A-Za-z]
if [ "$asciiA" -le "$asciic" -a "$asciic" -le "$asciiZ" ] ||
[ "$asciia" -le "$asciic" -a "$asciic" -le "$asciiz" ]
then ## toggle the sign bit (bit-6)
b="${b}$(printf "\x$(printf "%x" $((asciic ^ 1 << 5)))\n")"
else
b="$b$c" ## otherwise copy as is
fi
i=$(expr $i + 1)
done
echo "output: $b" ## output resluting string
The case change is affected by relying on a simple bit-toggle of the case-bit (bit-6) in the ASCII value of each upper or lower case character to change it from lower to upper or vice-versa. (and note, you can exchange the printf and bit-shift for tr of asciic as an alternative)
Example Use/Output
$ sh togglecase.sh
input : This Is My 10TH String
output: tHIS iS mY 10th sTRING
When you want to swab every second characters case, try this:
read -p " Enter string " str
for i in `seq 0 ${#str}`; do
rem=$(($i % 2 ))
if [ $rem -eq 0 ]
then
printf "%s" "${str:$i:1}"
else
fr=${str:$i:1}
printf "%s" "$(tr '[:upper:][:lower:]' '[:lower:][:upper:]' <<< "${str:$i:1}")"
fi
done
echo
EDIT: Second solution
Switch case of str and merge the old and new string.
#!/bin/bash
str="part is lowercase & PART IS UPPERCASE"
str2=$(tr '[:upper:][:lower:]' '[:lower:][:upper:]' <<< "${str}")
str_chopped=$(sed -r 's/(.)./\1\n/g' <<< "${str}");
# Will have 1 additional char for odd length str
# str2_chopped_incorrect=$(sed -r 's/.(.)/\1\n/g' <<< "${str2}");
str2_chopped=$(fold -w2 <<< "${str2}" | sed -nr 's/.(.)/\1/p' );
paste -d '\n' <(echo "${str_chopped}") <(echo "${str2_chopped}") | tr -d '\n'; echo

why the blackslash is not url encoded in this shell script?

I am trying to the url encode a string based on shell scripting.
I have downloaded a script from internet.
it is:
#!/bin/sh
url_encoder()
{
echo -n "$1" | awk -v ORS="" '{ gsub(/./,"&\n") ; print }' | while read l;
do
case "$l" in
[-_.~/a-zA-Z0-9] ) echo -n ${l} ;;
"" ) echo -n %20 ;;
* ) printf '%%%02X' "'$l"
esac
done
}
echo ""
}
The basic idea of the above codes is to
(1) convert a input string into the rows, each row has one character
(2) for each row, url encode the character
So If I run
$url_encoder "abc:"
the output would be "abc%3A", which is correct
But if I run
$url_encoder "\\" # I want to encode the backslash, so I use 2 "\" here
there is no output at all.
Do you know the reason why?
no need to use read which is slow, variable expansion can do a substring, no need to handle the space character specially, it can be handled as the default
url_encoder() {
local i str=$1 c
for ((i=0;i<${#str};i+=1)); do
c=${str:i:1}
case "$c" in
[-_.~/a-zA-Z0-9] ) echo -n "${c}" ;;
* ) printf '%%%02X' "'$c" ;;
esac
done
}
l='\'
printf '%%%02X' "'$l"
The reason why the backslash disapears is because it has a special meaning for read, -r option should be used to avoid.
https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-read
Note ~ should also be encoded http://www.rfc-editor.org/rfc/rfc1738.txt
printf argument starting with a quote (single or double), handles only ascii character "'$c" (<128).
url_encoder() { (
LC_ALL=C
str=$1
for ((i=0;i<${#str};i+=1)); do
c=${str:i:1}
if [[ $c = [-_./a-zA-Z0-9] ]]; then
echo -n "${c}"
elif [[ $c = [$'\1'-$'\x7f'] ]]; then
printf '%%%02X' "'$c"
else
printf '%%%s' $(echo -n "$c" | od -An -tx1)
fi
done
)}
Nahuel Fouilleul's helpful answer explains the problem with your approach (-r is missing from your read command, resulting in unwanted interpretation of \ chars.) and offers a more efficient bash solution.
Here's a more efficient, POSIX-compliant solution (sh-compatible) that performs the encoding with a single awk command, assuming that the input string is composed only of characters in the ASCII/Unicode code-point range between 32 and 127, inclusively:
#!/bin/sh
url_encoder()
{
awk -v url="$1" -v ORS= 'BEGIN {
# Create lookup table that maps characters to their code points.
for(n=32;n<=127;n++) ord[sprintf("%c",n)]=n
# Process characters one by one, either passing them through, if they
# need no encoding, or converting them to their %-prefixed hex equivalent.
for(i=1;i<=length(url);++i) {
char = substr(url, i, 1)
if (char !~ "[-_.~/a-zA-Z0-9]") char = sprintf("%%%x", ord[char])
print char
}
printf "\n"
}'
}

Bash variable substitution and strings

Let's say I have two variables:
a="AAA"
b="BBB"
I read a string from a file. This string is the following:
str='$a $b'
How to create a new string from the first one that substitutes the variables?
newstr="AAA BBB"
bash variable indirection whithout eval:
Well, as eval is evil, we may try to make this whithout them, by using indirection in variable names.
a="AAA"
b="BBB"
str='$a $b'
newstr=()
for cnt in $str ;do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt}
newstr+=($cnt)
done
newstr="${newstr[*]}"
echo $newstr
AAA BBB
Another try:
var1="Hello"
var2="2015"
str='$var1 world! Happy new year $var2'
newstr=()
for cnt in $str ;do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt}
newstr+=($cnt)
done
newstr="${newstr[*]}"
echo $newstr
Hello world! Happy new year 2015
Addendum As correctly pointed by #EtanReisner's comment, if your string do contain some * or other glob expendable stings, you may have to use set -f to prevent bad things:
cd /bin
var1="Hello"
var2="star"
var3="*"
str='$var1 this string contain a $var2 as $var3 *'
newstr=()
for cnt in $str ;do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt};
newstr+=("$cnt");
done;
newstr="${newstr[*]}"
echo "$newstr"
Hello this string contain a star as * bash bunzip2 busybox....zmore znew
echo ${#newstr}
1239
Note: I've added " at newstr+=("$cnt"); to prevent glob expansion, but set -f seem required...
newstr=()
set -f
for cnt in $str ;do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt}
newstr+=("$cnt")
done
set +f
newstr="${newstr[*]}"
echo "$newstr"
Hello this string contain a star as * *
Nota 2: This is far away from a perfect solution. For sample if string do contain ponctuation, this won't work again... Example:
str='$var1, this string contain a $var2 as $var3: *'
with same variables as previous run will render:
' this string contain a star as *' because ${!var1,} and ${!var3:} don't exist.
... and if $str do contain special chars:
As #godblessfq asked:
If str contains a line break, how do I do the substitution and preserve the newline in the output?
So this is not robust as every indirected variable must be first, last or space separated from all special chars!
str=$'$var1 world!\n... 2nd line...'
var1=Hello
newstr=()
set -f
IFS=' ' read -d$'\377' -ra array <<<"$str"
for cnt in "${array[#]}";do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt}
newstr+=("$cnt")
done
set +f
newstr="${newstr[*]}"
echo "$newstr"
Hello world!
... 2nd line...
As <<< inline string add a trailing newline, last echo command could be written:
echo "${newstr%$'\n'}"
The easiest solution is to use eval:
eval echo "$str"
To assign it to a variable, use command substitution:
replaced=$(eval echo "$str")
Disclaimer: I only discovered perl an hour ago. But this seems to work robustly, whatever special characters you throw at it:
newstr=$(a2="$a" b2="$b" perl -pe 's/\$a\b/$ENV{a2}/g; s/\$b\b/$ENV{b2}/g' <(echo -e "$str"))
Test:
a='A*A\nA'
b='B*B\nB'
str='$a $aa * \n $b $bb'
newstr=$(a2="$a" b2="$b" perl -pe 's/\$a\b/$ENV{a2}/g; s/\$b\b/$ENV{b2}/g' <(echo -e "$str"))
echo -e "$newstr"
Output:
A*A
A $aa *
B*B
B $bb
I'd use awk solution with awk-variables. This will allow passing a text containing special chars and subsitute any placeholder with it.
a workaround to recognize $ would be using [\x24]:
awk -v a="$a" -v b="$b" '{gsub("[\x24]a",a);gsub("[\x24]b",b); print}' <<< $str
here
-v defines variable a="$a"
[x24] is ASCII for $, so [x24]a equal to $a
gsub(x,y) - replaces x with y

On AIX, grep -B -A -m does not work. Any solution with sed or awk

I am on AIX system. But grep on AIX does not support -B, -A, -M on AIX system. Are there other solutions like awk or sed which can do the same job?
For example:
str1
str2
str3
str4
str9
str8
str1
str3
str2
I try to run grep str3 -m 1 -B 1 -A 1 to get:
str2
str3
str4
but it does not work on AIX. Is there any solution for sed or awk?
awk 'c&&c--;/str3/{print p;print $0;c=1}{p=$0}' file
You can use a circular buffer which you print when the match is found, followed by the matched line, followed by the additional lines.
#!/usr/bin/awk -f
BEGIN {
B = 4 # set these values like you would use for grep -B, -A, -m
A = 2
m = 3
patt = "999" # set this to your regex
i = 0
B++ # the buffer will hold B lines plus one for the match
}
{
a[i] = $0 # accumulate B (+1) lines in a circular buffer
i = (i + 1) % B
}
$0 ~ patt { # if the pattern is found print the contents of the buffer
for (j=1; j<=B; j++) {
print a[i]
i = (i + 1) % B
}
split("", a)
for (i=1; i<=A; i++) { # print the next A lines
getline
print
}
if (--m == 0) exit # output m matches then quit
print "---"
}
A more sophisticated script would accept options and arguments rather than having to edit it to change them.
As a compromise, you could rearrange things a little and pass arguments using AWK's -v option.
By the way, congratulations and +1 for providing sample input and output. You would be surprised how many questions are missing such obvious specifications...
This can be done with ed(1).
so ross$ cat >> cg.ed
/str3/-1;.+2p
so ross$ ed - cg.txt < cg.ed
str2
str3
str4
so ross$
You can make a script out of this that will take the filename and pattern as arguments:
so ross$ cat > cg.sh
#!/bin/sh
ed - $1 << eof
/$2/-1;.+2p
eof
so ross$ sh cg.sh cg.txt str3
str2
str3
str4
so ross$
I have a 78-line shell script that does the job. I later replaced it with a 114-line Perl script, but normally port GNU grep where I need the facility. The shell script follows - it uses a different nomenclature for the options (AFAIK, it predates the options on GNU grep; it was certainly developed independently of the options in GNU grep).
#!/bin/ksh
#
# #(#)$Id: old.sgrep.sh,v 1.5 2007/09/15 22:15:43 jleffler Exp $
#
# Special grep
# Finds a pattern and prints lines either side of the pattern
# Line numbers are always produced by ed (substitute for grep),
# which allows us to eliminate duplicate lines cleanly. If the
# user did not ask for numbers, these are then stripped out.
#
# BUG: if the pattern occurs in in the first line or two and
# the number of lines to go back is larger than the line number,
# it fails dismally.
set -- `getopt "f:b:hn" "$#"`
case $# in
0) echo "Usage: $0 [-hn] [-f x] [-b y] pattern [files]" >&2
exit 1;;
esac
# Tab required - at least with sed (perl would be different)
# But then the whole problem would be different if implemented in Perl.
number="'s/^\\([0-9][0-9]*\\) /\\1:/'"
filename="'s%^%%'" # No-op for sed
f=3
b=3
nflag=no
hflag=no
while [ $# -gt 0 ]
do
case $1 in
-f) f=$2; shift 2;;
-b) b=$2; shift 2;;
-n) nflag=yes; shift;;
-h) hflag=yes; shift;;
--) shift; break;;
*) echo "Unknown option $1" >&2
exit 1;;
esac
done
pattern="${1:?'No pattern'}"
shift
case $# in
0) tmp=${TMPDIR:-/tmp}/`basename $0`.$$
trap "rm -f $tmp ; exit 1" 0
cat - >$tmp
set -- $tmp
sort="sort -t: -u +0n -1"
;;
*) filename="'s%^%'\$file:%"
sort="sort -t: -u +1n -2"
;;
esac
case $nflag in
yes) num_remove='s/[0-9][0-9]*://';;
no) num_remove='s/^//';;
esac
case $hflag in
yes) fileremove='s%^$file:%%';;
no) fileremove='s/^//';;
esac
for file in $*
do
echo "g/$pattern/.-${b},.+${f}n" |
ed - $file |
eval sed -e "$number" -e "$filename" |
$sort |
eval sed -e "$fileremove" -e "$num_remove"
done
rm -f $tmp
trap 0
exit 0
this little coding will highlight the regexp and works like grep -A -B -m
Hope this helps.
Christian Knauer
#!/bin/bash
[ $# -lt 2 ] && printf "usage: %s <regex> <file> [[[back]forward]occurrence] \n" "$0" && exit 0
REGEX=$1
FILE=$2
BACK=${3:-1}
FORWARD=${4:-1}
STOP=${5:-1000000000}
awk -v bold=$'\e[1m' -v norm=$'\e[0m' -v back=$BACK -v forward=$FORWARD -v stop=$STOP 'BEGIN {cnt=0} { array[i++]=$0 }
END {
maxI=++i
for (j=0;j<maxI; j++) {
if (array[j] ~ /'"${REGEX}"'/) {
for (z=back;z>0; z--) {
print array[j-z]
}
printf bold > "/dev/stderr"
printf("%s\n", array[j])
printf norm > "/dev/stderr"
for (x=1;x<=forward; x++) {
print array[j+x]
}
cnt++
if (cnt == stop) {
break
}
}
}
}
' "$FILE"
Slightly simplified DigitallRoss' answer that additionally prints ALL matching lines, not only first one matching the str3 pattern:
echo ',g/str3/-1;+2p' | ed - myFile.txt
where
, # read all lines (from first to the very last)
g/str3/ # search for `str3` string
-1 # take 1 line BEFORE the matching one
;+2 # and also next 2 lines (so 3 lines in total)
p # and print them
If you want to print only FIRST occurrence then the command is simpler:
echo '/str3/-1;+2p' | ed - myFile.txt
where
/str3/ # find line matching the `str3` string
-1 # take 1 line BEFORE the matching one
;+2 # and also next 2 lines (so 3 lines in total)
p # and print them
If you need 3 lines after the str9, Here is an easy one
Input file
$cat <<'EOF' > strfile.txt
str1
str2
str3
str4
str9
str8
str1
str3
str2
EOF
Search for str9 and print the line + 3 lines ( including the file name. Handy while searching for a large number of file *.txt
$awk '/str9/{print FILENAME;n=NR+3} n>=NR' strfile.txt
strfile.txt
str9
str8
str1
str3

Resources