Coloring output of a script that overwrites lines? - bash

I'm using this to color the output of a script/command:
commandWithOutput | sed -r 's/(pattern)/'"${COLOR_RED}"'\1'"${COLOR_DEFAULT}"'/g'
(This will color all occurences of string "pattern" in the command's output.) And it works fine with traditional commands. However, if the script/command overwrites lines in its output (maybe this has more to do with a terminal/console than just standard output?), e.g.:
Building project X:
CXX Building file XYZ.cpp... [123/1034]
the behavior isn't as expected. My sed will still color the output but the overwriting doesn't work anymore, i.e.:
Building project X:
CXX Building file ABC.cpp... [1/1034]
CXX Building file DEF.cpp... [2/1034]
CXX Building file GHI.cpp... [3/1034]
CXX Building file JKL.cpp... [4/1034]
CXX Building file MNO.cpp... [5/1034]
// and so on...
CXX Building file XYZ.cpp... [123/1034]
Is there a way to color the output of a script/command that overwrites lines?

I've tried several different ideas ... IFS=$'\r' + OP's sed command ... trying to use an intermediate pipe (mkfifo) for processing the output from commandWithOutput ... a few attempts at trying to unbuffer stdout and/or stdin ... but (so far) could only get a awk solution to work, so fwiw ...
NOTE: I'm assuming OP's command is generating a \r when overwriting a line; if this is not the case the OP can try piping their command's output to | od -c to see what character is at the 'end of the line', with the idea being to use said character in place of my \r references (below).
First we'll write a small script to generate some data, (re)printing over the first few lines, and then printing some 'standalone' lines:
$ cat overwrite
#!/usr/bin/bash
for (( i=1 ; i<="${1}" ; i++ ))
do
printf "this is a test ... ${i}\r"
sleep 1
done
printf "\nanother test output \t and a tab\n"
echo "X."
Running the above generates the following output:
$ overwrite 3
this is a test ... 3 << this line is actually printed 3x times with suffixes of '1', '2' and '3'
another test output and a tab
X.
Running this through od shows the \r at the end of the first 3 lines:
$ overwrite 3 | od -c
0000000 t h i s i s a t e s t .
0000020 . . 1 \r t h i s i s a t
0000040 e s t . . . 2 \r t h i s i
0000060 s a t e s t . . . 3 \r \n
0000100 a n o t h e r t e s t o u t
0000120 p u t \t a n d a t a b \n
0000140 X . \n
0000143
We'll now look at one awk solution for recoloring a specific pattern in the output from our overwrite script ...
First we'll define the start and clear/reset variables for our desired color; for this exercise I'm going to use 'red':
$ myred=$(tput setaf 1) # set our highlight color to red
$ myreset=$(tput sgr0) # disable coloring
NOTE: There are a few ways to define these colors (and the disable/reset); I'll leave that up to the reader to pick what works best in their environment.
Here's one awk solution I found that works:
$ overwrite 3 | awk -v ptn="test" -v cstart="${myred}" -v creset="${myreset}" -v RS="[\n\r]" '{ sub(ptn,cstart ptn creset) ; printf $0 RT }'
Where:
-v ptn="test" - we want to recolor all instances of the string test; we'll pass this in as awk variable ptn
-v cstart="${myred}" - assign our highlight color code (red) to our awk variable cstart
-v creset="${myreset}" - assign our color clear/reset code to the awk variable creset
-v RS="[\n\r]" - redefine our input record separator as either \r or \n
sub(ptn,cstart ptn creset) - replace all instances of test with <red> + test + <reset>
printf $0 RT - print our new line; RT allows us to make use of the same RS that was used to parse out this record
Running the above generates:
this is a test ... 3 << this line is actually printed 3x times with suffixes of '1', '2' and '3', and the 'test' string printed in red
another test output and a tab << the 'test' string is printed in red
X.

Related

sed/awk between two patterns in a file: pattern 1 set by a variable from lines of a second file; pattern 2 designated by a specified charcacter

I have two files. One file contains a pattern that I want to match in a second file. I want to use that pattern to print between that pattern (included) up to a specified character (not included) and then concatenate into a single output file.
For instance,
File_1:
a
c
d
and File_2:
>a
MEEL
>b
MLPK
>c
MEHL
>d
MLWL
>e
MTNH
I have been using variations of this loop:
while read $id;
do
sed -n "/>$id/,/>/{//!p;}" File_2;
done < File_1
hoping to obtain something like the following output:
>a
MEEL
>c
MEHL
>d
MLWL
But have had no such luck. I have played around with grep/fgrep awk and sed and between the three cannot seem to get the right (or any output). Would someone kindly point me in the right direction?
Try:
$ awk -F'>' 'FNR==NR{a[$1]; next} NF==2{f=$2 in a} f' file1 file2
>a
MEEL
>c
MEHL
>d
MLWL
How it works
-F'>'
This sets the field separator to >.
FNR==NR{a[$1]; next}
While reading in the first file, this creates a key in array a for every line in file file.
NF==2{f=$2 in a}
For every line in file 2 that has two fields, this sets variable f to true if the second field is a key in a or false if it is not.
f
If f is true, print the line.
A plain (GNU) sed solution. Files are read only once. It is assumed that characters in File_1 needn't to be quoted in sed expression.
pat=$(sed ':a; $!{N;ba;}; y/\n/|/' File_1)
sed -E -n ":a; /^>($pat)/{:b; p; n; /^>/ba; bb}" File_2
Explanation:
The first call to sed generates a regular expression to be used in the second call to sed and stores it in the variable pat. The aim is to avoid reading repeatedly the entire File_2 for each line of File_1. It just "slurps" the File_1 and replaces new-line characters with | characters. So the sample File_1 becomes a string with the value a|c|d. The regular expression a|c|d matches if at least one of the alternatives (a, b, c for this example) matches (this is a GNU sed extension).
The second sed expression, ":a; /^>($pat)/{:b; p; n; /^>/ba; bb}", could be converted to pseudo code like this:
begin:
read next line (from File_2) or quit on end-of-file
label_a:
if line begins with `>` followed by one of the alternatives in `pat` then
label_b:
print the line
read next line (from File_2) or quit on end-of-file
if line begins with `>` goto label_a else goto label_b
else goto begin
Let me try to explain why your approach does not work well:
You need to say while read id instead of while read $id.
The sed command />$id/,/>/{//!p;} will exclude the lines which start
with >.
Then you might want to say something like:
while read id; do
sed -n "/^>$id/{N;p}" File_2
done < File_1
Output:
>a
MEEL
>c
MEHL
>d
MLWL
But the code above is inefficient because it reads File_2 as many times as the count of the id's in File_1.
Please try the elegant solution by John1024 instead.
If ed is available, and since the shell is involve.
#!/usr/bin/env bash
mapfile -t to_match < file1.txt
ed -s file2.txt <<-EOF
g/\(^>[${to_match[*]}]\)/;/^>/-1p
q
EOF
It will only run ed once and not every line that has the pattern, that matches from file1. Like say if you have a to z from file1,ed will not run 26 times.
Requires bash4+ because of mapfile.
How it works
mapfile -t to_match < file1.txt
Saves the entry/value from file1 in an array named to_match
ed -s file2.txt point ed to file2 with the -s flag which means don't print info about the file, same info you get with wc file
<<-EOF A here document, shell syntax.
g/\(^>[${to_match[*]}]\)/;/^>/-1p
g means search the whole file aka global.
( ) capture group, it needs escaping because ed only supports BRE, basic regular expression.
^> If line starts with a > the ^ is an anchor which means the start.
[ ] is a bracket expression match whatever is inside of it, in this case the value of the array "${to_match[*]}"
; Include the next address/pattern
/^>/ Match a leading >
-1 go back one line after the pattern match.
p print whatever was matched by the pattern.
q quit ed

Use `sed` to replace text in code block with output of command at the top of the code block

I have a markdown file that has snippets of code resembling the following example:
```
$ cat docs/code_sample.sh
#!/usr/bin/env bash
echo "Hello, world"
```
This means there there's a file at the location docs/code_sample.sh, whose contents is:
#!/usr/bin/env bash
echo "Hello, world"
I'd like to parse the markdown file with sed (awk or perl works too) and replace the bottom section of the code snippet with whatever the above bash command evaluates to, for example whatever cat docs/code_sample.sh evaluates to.
Perl to the rescue!
perl -0777 -pe 's/(?<=```\n)^(\$ (.*)\n\n)(?^s:.*?)(?=```)/"$1".qx($2)/meg' < input > output
-0777 slurps the whole file into memory
-p prints the input after processing
s/PATTERN/REPLACEMENT/ works similarly to a substitution in sed
/g replaces globally, i.e. as many times as it can
/m makes ^ match start of each line instead of start of the whole input string
/e evaluates the replacement as code
(?<=```\n) means "preceded by three backquotes and a newline"
(?^s:.*?) changes the behaviour of . to match newlines as well, so it matches (frugally because of the *?) the rest of the preformatted block
(?=```) means "followed by three backquotes`
qx runs the parameter in a shell and returns its output
A sed-only solution is easier if you have the GNU version with an e command.
That said, here's a quick, simplistic, and kinda clumsy version I knocked out that doesn't bother to check the values of previous or following lines - it just assumes your format is good, and bulls through without any looping or anything else. Still, for my example code, it worked.
I started by making an a, a b, and an x that is the markup file.
$: cat a
#! /bin/bash
echo "Hello, World!"
$: cat b
#! /bin/bash
echo "SCREW YOU!!!!"
$: cat x
```
$ cat a
foo
bar
" b a z ! "
```
```
$ cat b
foo
bar
" b a z ! "
```
Then I wrote s which is the sed script.
$: cat s
#! /bin/env bash
sed -En '
/^```$/,/^```$/ {
# for the lines starting with the $ prompt
/^[$] / {
# save the command to the hold space
x
# write the ``` header to the pattern space
s/.*/```/
# print the fabricated header
p
# swap the command back in
x
# the next line should be blank - add it to the current pattern space
N
# first print the line of code as-is with the (assumed) following blank line
p
# scrub the $ (prompt) off the command
s/^[$] //
# execute the command - store the output into the pattern space
e
# print the output
p
# put the markdown footer back
s/.*/```/
# and print that
p
}
# for the (to be discarded) existing lines of "content"
/^[^`$]/d
}
' $*
It does the job and might get you started.
$: s x
```
$ cat a
#! /bin/bash
echo "Hello, World!"
```
```
$ cat b
#! /bin/bash
echo "SCREW YOU!!!!"
```
Lots of caveats - better to actually check that the $ follows a line of backticks and is followed by a blank line, maybe make sure nothing bogus could be in the file to get executed... but this does what you asked, with (GNU) sed.
Good luck.
A rare case when use of getline would be appropriate:
$ cat tst.awk
state == "importing" {
while ( (getline line < $NF) > 0 ) {
print line
}
close($NF)
state = "imported"
}
$0 == "```" { state = (state ? "" : "importing") }
state != "imported" { print }
$ awk -f tst.awk file
See http://awk.freeshell.org/AllAboutGetline for getline uses and caveats.

Bash/Sed - multiline sed operation printing lines out of order

I am having some trouble with using sed to edit a log file. I have built it into a function which is supposed to replace the text between two search strings with the output from another function. It is almost working correctly, but is printing the lines to the log file out of order. For the life of me I can't figure out why, and most adjustments I have made while trying to fix it have actually had less desirable results.
My sed function:
log_edit(){
"$3" > temp.txt
sed -i -n "/$1/{
:loop
n
/$2/!b loop
x
r temp.txt
G
s/$2/\n\n&/
}
p" "$FILE"
rm temp.txt
}
I am using the "=== text ===" dividers as my start and stop strings to pass along to the function, and using the same functions that built the log in the first place to fill the temporary text file.
The problem is occurring somewhere near/related to the 'G' command. Rather than appending the hold pattern line to the end of the string, it appears to be attaching it to the beginning of the string.
Original log sample/Desired output:
=== Metech ITAMS Log ===
Metech Recycling
ITAMS Hardware Report
Date: Thu Mar 2 08:01:38 PST 2017
Tech: SP
=== Manufacturer Information ===
# dmidecode 2.12
...
Unfortunately, the output I am getting looks like this:
=== Manufacturer Information ===
=== Metech ITAMS Log ===
Metech Recycling
ITAMS Hardware Report
Date: Fri Mar 3 09:39:02 PST 2017
Tech: SS
# dmidecode 2.12
...
Would someone be able to help me understand what I'm doing wrong, or propose a fix? This is my first question ever to SO, if more information is necessary I am happy to provide it. Thanks in advance.
Edit #1: As requested a snippet of the code that calls the function:
2)
printf "\n"
text_prompt "Please enter Tech initials: "
set_tech_id
text_prompt "Please enter Traveler ID: "
set_travel_id
mv "$FILE" "$TRAVEL_ID $TECH_INITIALS"
FILE="$TRAVEL_ID $TECH_INITIALS"
log_edit "=== Metech ITAMS Log ===" \
"=== Manufacturer Information ===" "print_header"
unset TECH_INITIALS
unset TRAVEL_ID
;;
This is part of a menu function, and it would be overkill to include the whole thing, just be aware that there will be several calls to log_edit with different start/stop strings (though all follow the === === pattern), but usually calling different functions to fill the temp.txt with.
Edit 2: For added clarity, I thought I should add the function being called with $3:
print_header(){ #Prints log header.
print_div "Metech ITAMS Log"
printf "Metech Recycling\nITAMS Hardware Report\nDate: $(date)\nTech: %s\n" \
"$TECH_INITIALS"
}
and print_header calls print_div:
print_div(){ #Prints a divider. Required parameter: $1=Text for divider.
printf "\n=== %s ===\n\n" "$1"
}
Edit 3: For question clarity, my issue is that the $2 string is being written to the log before the contents of temp.txt, rather then after.
Final Edit: A solution was found. I thought I would post the working code below just in case it's helpful to others. A big portion of my problem was a misunderstanding with how sed uses the 'r' command. There's another part to this solution that came from the accepted answer that I still don't understand, and that is the substitute commands that add backslashes, this was key to making it work. I don't know why it works, but it does.
log_edit() { #Works!!
"$3" > temp.txt
sed -i -n '/^'"$1"'$/ {
:loop
n
/^'"$2"'$/!b loop
i\
'"$(sed 's/\\/\\&/g;s/$/\\/' -- "temp.txt")"'
#Blank line terminates i command.
}
p' "$FILE"
rm temp.txt
}
The r command copies out the file before the next read, not when it is evaluated, and does not modify pattern-space. However the file can be inserted into the script as part of an i command:
log_edit() {
sed -n '/^'"$1"'$/ {
p
:loop
n
/^'"$2"'$/!bloop
i\
'"$("$3" | sed 's/^[[:space:]]/\\&/;s/\\/\\&/g;s/$/\\/')"'
# The blank line above is part of the `i' command,
# and appends a newline to the inserted text.
}
p' "$FILE" > "$FILE.mod" && mv -f -- "$FILE.mod" "$FILE"
}
The command-substitution "$("$3" | sed '...')" filters the output
of $3 for use with sed's i command. The i command prints
a series of lines will all but the last ending with a \.
$ echo three | sed 'i\
> one\
> two
> '
one
two
three
Looks like just a few things out of order there. Try this:
log_edit(){
"$3" > text.tmp
sed -i -n "/$1/{
r text.tmp
:loop
N
/$2/!b loop
s/.*\n/\n\n/g
}
p
" "$FILE"
rm text.tmp
}
print_header(){ #Prints log header.
print_div "Metech ITAMS Log"
printf "Metech Recycling\nITAMS Hardware Report\nDate: $(date)\nTech:%s" "$TECH_INITIALS"
}
print_div(){ #Prints a divider. Required parameter: $1=Text for divider.
printf "\n=== %s ===\n\n" "$1"
}
log_edit "=== Metech ITAMS Log ===" "=== Manufacturer Information ===" "print_header"
Try the csplit program, which can divide a file up into sections according to a pattern:
csplit $3 "/\($1\|$2\)/" "{*}"
This means take file $3, and break it into files xxNN (where NN starts at 00 and goes up) according to sections demarcated by an unlimited number ({*}) of patterns $1 OR $2 (two alternate patterns, separated by \| and grouped by escaped parentheses). The demarcation lines will remain in the output. You can then write ancillary code to delete the files you don't want. You can also change the name of the output filename and pattern.
# cat foo
a
b
#
c
d
%
e
f
#
g
h
%
i
j
# csplit foo '/\(#\|%\)/' '{*}'
4
6
6
6
6
# more xx0*
::::::::::::::
xx00
::::::::::::::
a
b
::::::::::::::
xx01
::::::::::::::
#
c
d
::::::::::::::
xx02
::::::::::::::
%
e
f
::::::::::::::
xx03
::::::::::::::
#
g
h
::::::::::::::
xx04
::::::::::::::
%
i
j
Note: You'll need to tweak it if your demarcation lines can repeat/occur out of order. This is very simple; the breaks occur at any point that one pattern OR the other is seen, regardless of order.

How to line wrap output in bash?

I have a command which outputs in this format:
A
B
C
D
E
F
G
I
J
etc
I want the output to be in this format
A B C D E F G I J
I tried using ./script | tr "\n" " " but all it does is remove n from the output
How do I get all the output in one line. (Line wrapped)
Edit: I accidentally put in grep while asking the question. I removed
it. My original question still stands.
The grep is superfluous.
This should work:
./script | tr '\n' ' '
It did for me with a command al that lists its arguments one per line:
$ al A B C D E F G H I J
A
B
C
D
E
F
G
H
I
J
$ al A B C D E F G H I J | tr '\n' ' '
A B C D E F G H I J $
As Jonathan Leffler points out, you don't want the grep. The command you're using:
./script | grep tr "\n" " "
doesn't even invoke the tr command; it should search for the pattern "tr" in files named "\n" and " ". Since that's not the output you reported, I suspect you've mistyped the command you're using.
You can do this:
./script | tr '\n' ' '
but (a) it joins all its input into a single line, and (b) it doesn't append a newline to the end of the line. Typically that means your shell prompt will be printed at the end of the line of output.
If you want everything on one line, you can do this:
./script | tr '\n' ' ' ; echo ''
Or, if you want the output wrapped to a reasonable width:
./script | fmt
The fmt command has a number of options to control things like the maximum line length; read its documentation (man fmt or info fmt) for details.
No need to use other programs, why not use Bash to do the job? (-- added in edit)
line=$(./script.sh)
set -- $line
echo "$*"
The set sets command-line options, and one of the (by default) seperators is a "\n". EDIT: This will overwrite any existing command-line arguments, but good coding practice would suggest that you reassigned these to named variables early in the script.
When we use "$*" (note the quotes) it joins them alll together again using the first character of IFS as the glue. By default that is a space.
tr is an unnecessary child process.
By the way, there is a command called script, so be careful of using that name.
If I'm not mistaken, the echo command will automatically remove the newline chars when its argument is given unquoted:
tmp=$(./script.sh)
echo $tmp
results in
A B C D E F G H I J
whereas
tmp=$(./script.sh)
echo "$tmp"
results in
A
B
C
D
E
F
G
H
I
J
If needed, you can re-assign the output of the echo command to another variable:
tmp=$(./script.sh)
tmp2=$(echo $tmp)
The $tmp2 variable will then contain no newlines.

Reading a subset of the lines in a text file, with bash

I have a file
line a - this is line a
line b - this is line b
line c - this is line c
line d - this is line d
line e - this is line e
The question is: How can I output the lines starting from "line b" till "line d" using bash commands?
I mean, to obtain:
"line b - this is line b
line c - this is line c
line d - this is line d"
sed -n '/line b/,/line d/p' file
Your example is not enough to infer what you want in the general case, but assuming you want to remove the first and last line, you can simply use
tail -n+2 $filename | head -n-1
Here tail -n+2 prints all the lines starting from the second, and head -n-1 prints all the lines except the last.
for your set of sample data:
awk '/line b/,/line d/' file
Or
awk '/line d/{f=0;print}/line b/{f=1}f' file
If by bash, you mean actually bash alone, I can't help you. You really should be using the right tools for the job. If you mean standard UNIX utilities that you can call from bash, I would be using awk for that.
echo 'line a - this is line a
line b - this is line b
line c - this is line c
line d - this is line d
line e - this is line e' | awk '
BEGIN {e=0}
/^line b/ {e=1}
/^line d/ {if (e==1) {print;exit}}
{if (e==1) print}
'
This outputs:
line b - this is line b
line c - this is line c
line d - this is line d
The way it works is simple.
e is the echo flag, initially set to false (0).
when you find line b, set echo to true (1) - don't print yet. That will be handled by the last bullet point below.
when you find line d and echo is on, print it and exit.
when echo is on, print the line (this includes line b).
I've made an assumption here that you don't want to exit on a line d unless you're already echoing. If that's wrong, move the exit outside of the if statement for line d:
/^line d/ {if (e==1) print;exit}
Then, if you get a line d before your line b, it will just exit without echoing anything.
The "/^line X/"-type clauses can be made very powerful to match pretty well anything you can throw at it.
You can do it using bash alone, though I agree with Pax that using other tools is probably a better solution. Here's a bash-only solution:
while read line
do
t=${line#line b}
if test "$t" != "$line"
then
echo $line
while read line
do
echo $line
t=${line#line d}
if test "$t" != "$line"
then
exit 0
fi
done
fi
done
Another approach which depends on what you mean:
pcregrep -m 'line b - this is line b
line c - this is line c
line d - this is line d' file

Resources