I'm trying to make 'perfect' command to show any .php file in dir or subdirs that contain eval code.
Since there are many many false positives, I'm after solution that would strip at least most obvious of them - so my target is:
word eval, followed by any whitepace char including newline zero to unlimited times, followed by open bracket char (;
Here are my shots:
find . -type f -exec grep -l "eval\s*(" {} \; | grep ".php"
Works great but somehow \s* here doesn't match newline characters, so
eval
("some nasty obfuscated code");
is bellow the radar.
I've also tried with:
find . -type f -exec grep -l "eval[[:space:]]*(" {} \; | grep ".php"
with same results.
If I did understand you correct, I believe this line here to be what you're looking for:
find . -name '*.php' -exec grep -Ezl 'eval\s*\(' {} +
the -z is what you've been missing, see explanation below.
and of course you could give the find command whatever other root rather than . and just add arguments and conditions according to where you are looking in and what you are looking for.
That was it. From here on, explanations:
The find command
It would probably be faster in most cases to first search for files with .php extension, and then search only within these files for your regular expression. The -name '*.php' part gives us this behavior by searching only for files with a file name ending with '.php'.
-exec allows us to execute a command using the output of the find command (file names). We are using it in order to execute grep for all php files.
This syntax {} + in the end of the line, creates one long list of file names as arguments for the grep command, instead of executing grep separately for every file.
The grep command
-E: Interpret PATTERN as an extended regular expression (copied from the grep man page)
-z: Treat the input as a set of lines, each terminated by a zero byte instead of a newline (grep man page). That means that for a normal textual file, the whole file would be treated as one long line. This behavior allows you to use multi-lined regular expressions.
-l: tells grep to only show the filenames for all the files matching the search, and not to show the matching lines.
The regular expression:
'eval' just matches the word eval.
'\s' matches any whitespace character, and the '*' after it means it could appear zero or more times. This '\(' matches an actual bracket, which in this case needs escaping (and that's what the \ is for).
have fun!
Simple Version:
For simplicity sake, to cater for your need, but using awk instead of grep (if this is possible), then for php files in /tmp/, you could simply;
awk -v RS="^$" '/eval[[:space:]]*\(/ { print FILENAME }' /tmp/*.php
And that will print the files that match.
If you need to use the output of find:
find /tmp/ -iname "*.php" -print | while read file ; do awk -v RS="^$" '/eval[[:space:]]*\(/ { print FILENAME }' "$file" ; done
The above is simple and works even with busybox and basic versions of awk.
Alternate (With matches)
This part of the answer may seem absurd to some, but enough experience with searching for whitespace, and doing serialisation in the shell, the amount of "gotcha's" become evident, and the need for a working solution causes the preference for built-in one liners to take a back seat.
This might also help others stumbling across a similar need, but requiring easy to read line previews, maybe for parsing, or simplicity:
NOTE 1: This solution works in sh/ash/busybox as well as bash (the external binary xxd would still be needed)
NOTE 2: For BSD grep, substitute -P with -E. Using -E on a GNU grep that has support for -P, seems to not yield the same lookahead matches
Example Test File
Take this test file (with special characters notated in place), plus 2 other test files that are located in /tmp/ for this example:
find /tmp/ -iname "*.php" -print \
| while read file ; do hexdump -ve '1/1 " %02X"' "$file" \
| sed -E "s/($)/ 0A/g" \
| grep -P -o "65 76 61 6C( 09| 0A| 0B| 0C| 0D| 20)*? 28 22.+?0A" \
| sed -E -e 's/ //g' \
| sed -E -e 's/(0A)+([^$])/20\2/g' \
| sed -E -e 's/(09|0B|0C|0D|20)+/20/g' \
| xxd -r -p \
| grep -i "eval" && printf "$file matches\n\n" ; done
Will return the matches, from eval, to the end of the line where the (" was matched, substituting line breaks and spaces for a single space for readability :
eval ("some nasty obfuscated code (LF / LINE FEED)");
eval ("some nasty obfuscated code (HT / TAB)");
eval ("some nasty obfuscated code (SP / SPACE)");
eval ("some nasty obfuscated code (FF / FORM FEED)");
eval ("some nasty obfuscated code (CR / CARRIAGE RETURN)");
eval ("some nasty obfuscated code (VT / VERTICAL TAB)");
eval ("some nasty obfuscated code (LF > HT > FF > CR > LF > LF > HT > VT > LF > HT > SP)");
eval ("some nasty obfuscated code (VT / VERTICAL TAB)");
/tmp/eval.php matches
eval ("some nasty obfuscated code (LF / LINE FEED)");
/tmp/eval_no_trailing_line_feed.php matches
eval("\$str = \"$str\";");
/tmp/eval_w3_example.php matches
For just the file matches using this method (maybe to allow for a "-v" option for example), just change grep -i on the last line to grep -iq
Explanation:
find /tmp/ -iname "*.php" -print \ : Find .php files in /tmp/
| while read file ; do hexdump -ve '1/1 " %02X"' "$file" \ : hexdump each resulting file, and output in single space separated bytes (to avoid any matching from the second character of one byte to the first char of another byte)
| sed -E "s/($)/ 0A/g" \ : Put a single 0A (line feed) at the very end of the file that matches - This means it will match a file that does not have a trailing line feed (sometimes can cause some issues with text processing)
| grep -P -o "65 76 61 6C( 09| 0A| 0B| 0C| 0D| 20)*? 28 22.+?0A" \ : Return only match (note that grep adds a line break to each match)
6576616C : eval
09 : horizontal TAB
0A : line feed
0B : vertical TAB
0C : form feed
0D : carriage return
20 : plane SPACE
2822 : ("
| sed -E -e 's/ //g' \ : Remove all spaces between bytes (may not have been needed in the end)
| sed -E -e 's/(0A)+([^$])/20\2/g' \ : Look for any repeated occurrences of 0A (line feed), as long as they are not the line feed at the end of the line, and replace them with a single space (20)
| sed -E -e 's/(09|0B|0C|0D|20)+/20/g' \ : Look for any of the white space characters above, and replace them with a space, for readability
| xxd -r -p \ : Revert back from hex
| grep -i "eval" && printf "$file matches\n\n" ; done : Print the match, and the file name (the && means that printf will only print the file match, if the output of grep was 0 (success), therefore it won't simply print every file in the loop. (as noted before, adding -q into this grep will still evaluate for the purpose of printf, but will not output the matching lines.
Related
I am using grep to search through text files containing 88 character long MRZs (machine readable zones). Within the text file they are preceeded by a semicolon.
I only want to get the substring of characters 3-5 from the string.
This is my pattern:
egrep --include *.txt -or . -e ";[A-Z][A-Z0-9<][A-Z<]{3}"
This is a textfile:
text is here;P<RUSIVAN<<DEL<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<F64D123456RUS7404124F131009734P41234<<<<<<<8 ;2019-02-08
This is my output:
;P<RUS
This is my desired output:
RUS
The semicolon introduces the MRZ. It starts with a uppercase letter, followed by either an uppercase letter, a digit or a filler character <. Then follows the 3 digit country code that can contain uppercase letters or filler characters <.
This pattern works fine, but what I only want returned is the last 3 digits I am quantifying. Is there a way to get only the last 3 characters of a matching pattern?
In the sample text file the desired output would be RUS.
Thank you!
If you could use GNU Grep, you can make use of \K which will no longer include any of the previous matched characters in the match and then match your character class 3 times:
grep -roP --include=*.txt ";[A-Z][A-Z0-9<]\K[A-Z<]{3}"
Is this all you're trying to do?
$ awk -F';' '{print substr($2,3,3)}' file
RUS
$ sed -E 's/[^;]*;..(.{3}).*/\1/' file
RUS
If not then edit your question to provide more truly representative sample input/output.
The UNIX command to find files is named find, btw, not grep. I know the GNU guys added a bunch of options for finding files to grep but just don't use them as they make your grep command unnecessarily complicated (and inconsistent with the other UNIX text processing tools) as it then needs arguments to find files as well as to g/re/p within the files. So your command line if you're using grep should be:
find . -name '*.txt' -exec grep 'stuff' {} +
not:
egrep --include *.txt -or . -e 'stuff'
and do the same for any other tool:
find . -name '*.txt' -exec grep 'stuff' {} +
find . -name '*.txt' -exec sed 'stuff' {} +
find . -name '*.txt' -exec awk 'stuff' {} +
I have to replace following String
//#Config(manifest
with below string,
#Config(manifest
So this i created following regex
\/\/#Config\(manifest
And tried
grep -rl \/\/#Config\(manifest . | xargs sed -i "\/\/#Config\(manifest#Config\(manifest/g"
But i am getting following error:
sed: -e expression #1, char 38: Unmatched ( or \(
I have to search recursively and do this operation, though i am stuck with above error.
grep -rl '//#Config(manifest' | xargs sed -i 's|//#Config(manifest|#Config(manifest|g'
Specifying . for current directory is optional for grep -r
sed allows Any character other than backslash or newline to be used as delimiter
Edit
If file name contains spaces, use
grep -rlZ '//#Config(manifest' | xargs -0 sed -i 's|//#Config(manifest|#Config(manifest|g'
Explanation (assumes GNU version of commands)
grep
-r performs recursive search
-l option outputs only filenames instead of matched patterns
-Z outputs a zero byte (ASCII NUL character) after each file name instead of usual newline
'pattern' by default, grep uses BRE (basic regular expression) where characters like ( do not have special meaning and hence need not be escaped
xargs -0 tells xargs to separate arguments by the ASCII NUL character
sed
-i inplace edit, use -i.bkp if you want to create backup of original files
s|pattern|replace|g the g flag tells sed to search and replace all occurrences. sed also defaults to BRE and so no need to escape (. Using \( would mean start of capture groups and hence the error when it doesn't find the closing \)
I am attempting to use regex to match everything before the /, but when i try the following I get nothing outputted. I double checked my regex and seems okay, but not sure why it isn't working..
[user#user my_dir]$ tar -tf abc_de123_01.02.03.4.tgz | grep -m1 /
abc_de123_01.02.03.4/abcde.ini
[user#user my_dir]$ tar -tf abc_de123_01.02.03.4.tgz | grep -m1 .*\/
[user#user my_dir]$ tar -tf abc_de123_01.02.03.4.tgz | grep -m1 /$
expected output:
abc_de123_01.02.03.4/
There are three problems here.
One problem is that * has a special meaning to your shell; if you run echo grep -m1 .*\/, you'll see that your shell is expanding .*\/ in a way you don't expect.
One problem is that grep prints matching lines by default. If you want it to print just the matching part of a line, you need the -o flag.
One problem that's not actually breaking your command, but that you should nonetheless fix, is that your shell uses \ as a quoting (escape) character, so \/ actually means just /. (The reason this doesn't break anything is that / isn't special to grep anyway, so you didn't actually need the \ for anything.)
So:
grep -m1 -o '.*/'
which finds the first line containing /, and prints everything up through the last / on that line.
Incidentally, / is not a backslash, but simply a slash (or sometimes forward slash). A backslash is \.
Grep by default works on line wise operations. If you need only part of the string in all the lines you might use cut instead.
tar -tf abc_de123_01.02.03.4.tgz | cut -d'/' -f1
Now if you need only the first part of the first match sed come in hand:
tar -tf abc_de123_01.02.03.4.tgz | sed "1q;d" | cut -d'/' -f1
I've got a folder full of files with the names ab1234, abc5678, etc., and I want to switch them to abc3412, abc7856, etc. – just swap the last two characters out with the second-to-last two characters. The filenames are all in this format, no surprises. What's the easiest way to do this with a regex?
Use perl rename?
rename 's/(..)(..)$/$2$1/' *
Depending on your platform, you may have a rename utility that can directly do what you want.
For instance, anishsane's answer shows an elegant option using a Perl-based renaming utility.
Here's a POSIX-compliant way to do it:
printf '"%s"\n' * | sed 'p; s/\(..\)\(..\)"$/\2\1"/' | xargs -L 2 mv
printf '"%s"\n' * prints all files in the current folder line by line (if there are subdirs., you'd have to exclude them), enclosed in literal double-quotes.
sed 'p; s/\(..\)\(..\)"$/\2\1"/' produces 2 output lines:
p prints the input line as-is.
s/\(..\)\(..\)"$/\2\1"/' matches the last 2 character pairs (before the closing ") on the input lines and swaps them.
The net effect is that each input line produces a pair of output lines: the original filename, and the target filename, each enclosed in double-quotes.
xargs -L 2 mv then reads pairs of input lines (-L 2) and invokes the mv utility with each line as its own argument, which results in the desired renaming. Having each line enclosed in double-quotes ensures that xargs treats them as a single argument, even if they should contain whitespace.
Tip of the hat to anishsane for the enclose-in-literal-double-quotes approach, which makes the solution robust.
Note: If you're willing to use non-POSIX features, you can simplify the command as follows, to bypass the need for extra quoting:
GNU xargs:
printf '%s\n' * | sed 'p; s/\(..\)\(..\)$/\2\1/' | xargs -d '\n' -L 2 mv
Nonstandard option -d '\n' tells xargs to not perform word splitting on lines and treat each line as a single argument.
BSD xargs (also works with GNU xargs):
printf '%s\n' * | sed 'p; s/\(..\)\(..\)$/\2\1/' | tr '\n' '\0' | xargs -0 -L 2 mv
tr '\n' '\0' replaces newlines with \0 (NUL) chars, which is then specified as the input-line separator char. for xargs with the nonstandard -0 option, again ensuring that each input line is treated as a single argument.
I'd like to have a substitute or print style command with a regex working across lines. And lines retained.
$ echo -e 'a\nb\nc\nd\ne\nf\ng' | tr -d '\n' | grep -or 'b.*f'
bcdef
or
$ echo -e 'a\nb\nc\nd\ne\nf\ng' | tr -d '\n' | sed -r 's|b(.*)f|y\1z|'
aycdezg
i'd like to use grep or sed because i'd like to know what people would've done before awk or perl ..
would they not have? was .* not available? had they no other equivalent?
to possibly modify some input with a regex that spans across lines, and print it to stdout or output to a file, retaining the lines.
This should do what you're looking for:
$ echo -e 'a\nb\nc\nd\ne\nf\ng' | sed ':a;$s/b\([^f]*\)f/y\1z/;N;ba'
a
y
c
d
e
z
g
It accumulates all the lines then does the replacement. It looks for the first "f". If you want it to look for the last "f", change [^f] to ..
Note that this may make use of features added to sed after AWK or Perl became available (AWK has been around a looong time).
Edit:
To do a multi-line grep requires only a little modification:
$ echo -e 'a\nb\nc\nd\ne\nf\ng' | sed ':a;$s/^[^b]*\(b[^f]*f\)[^f]*$/\1/;N;ba'
b
c
d
e
f
sed can match across newlines through the use of its N command. For example, the following sed command will replace bar followed a newline followed by foo with ###:
$ echo -e "foo\nbar\nbaz\nqux" | sed 'N;s/bar\nbaz/###/;P;D'
foo
###
qux
The N command will append the next input line to the current pattern space separated by an embedded newline (\n)
The P command will print the current pattern space up to and including the first embedded newline.
The D command will delete up to and including the first embedded newline in the pattern space. It will also start next cycle but skip reading from the input if there is still data in the pattern space.
Through the use of these 3 commands, you can essentially do any sort of s command replacement looking across N-lines.
Edit
If your question is how can I remove the need for tr in the two examples above and just use sed then here you go:
$ echo -e 'a\nb\nc\nd\ne\nf\ng' | sed ':a;N;$!ba;s/\n//g;y/ag/yz/'
ybcdefz
Proven tools to the rescue.
echo -e "foo\nbar\nbaz\nqux" | perl -lpe 'BEGIN{$/=""}s/foo\nbar/###/'