2

here's my trouble.

Let me describe the situation: I work on a large number of servers which contain a large number of customers; all of them have a webspace, but only some of them are using Wordpress.

For security reasons, I would like to get the Wordpress version of all the Wordpress installations of a specific server (I would apply the process to others after I found a solution).

I found the file /wp-includes/version.php which contains the line "$wp_version = 'x.x.x'" - seems perfect for keeping an eye on the installations of the customers and notify them when their version is dangerous.

I began to write of a command line I would then put into a cron to retrieve results, once a month:

for files in $(find /home -type f -iwholename "*/wp-includes/version.php") ; do domain=$(echo $files | awk -F'domains/' '{print $2}' | awk -F'/' '{print $1}') ; version=$(grep "wp_version = " $files | awk -F'=' '{print $2}') ; echo "$domain : $version" ; done | grep -v "4.8" | sed -e "s/'/ /g" -e "s/;/ /g"

The line above produces a result of this kind:

domain.com : 4.1.3
another.com : 4.7.6
again.com : 4.7.6

Looks like I got the result I wanted - but, as you can see, this command line is REALLY long and even if I tried different versions, I didn't find an elegant variant.

Of course, I tried different versions until this result, and this is the only one working correctly; and I haven't been able to find something similar, but better. The real problem is, I think, related to the variable definitions from a result; I am learning awk but it's something like ugly, even if it's an awesome tool. The sed command at the end clears the raw result that looks like this:

domain.com : '4.1.3';
another.com : '4.7.6';
again.com : '4.7.6';

The wp-includes/version.php looks similar to it:

<?php
/**
 * The WordPress version string
 *
 * @global string $wp_version
 */
$wp_version = '3.9'; # for example
...

Could you please tell me if you see any "optimization"? This is more about education than real trouble.

Edit : the most optimized version for the moment is the following one in my opinion (clear enough for easy modifications, clean result, not so hard-to-understand functions):

for f in $(find /home -iwholename "*/wp-includes/version.php") ; do d=$(echo $f | awk -F'domains/' '{print $2}' | cut -d"/" -f1) ; v=$(grep '$wp_version = ' $f | sed -e 's/$wp_version = //g' -e "s/'//g" -e 's/;//g') ; echo "$v - $d" ; done
Atille
  • 23
  • 4

3 Answers3

1
version=$( awk -F"[\"']" '/^\$wp_version/{print $2}' "$files" )

To get version

$ cat version.php 
<?php
/**
 * The WordPress version string
 *
 * @global string $wp_version
 */
$wp_version = '3.9'; # for example
...

# used field separator either single quote or double quote
$ awk -F"[\"']" '/^\$wp_version/{print $2}' version.php 
3.9

For domain you may change from

domain=$(echo $files | awk -F'domains/' '{print $2}' | awk -F'/' '{print $1}') ;

To

domain=$(awk -F'domains/' '{split($2,tmp,/\//); print tmp[1]}' <<<"$files")

Suppose if you got path something like

$ files="path/to/domains/domain.com/wordpress/wp-includes/version.php"
$ awk -F'domains/' '{split($2,tmp,/\//); print tmp[1]}' <<<"$files"
domain.com
Akshay Hegde
  • 16,536
  • 2
  • 22
  • 36
  • 1
    Hello ; looks clearly better than my pipe system, I take notes. Thanks! – Atille Oct 27 '17 at 03:09
  • for domain `domain=$(awk -F'domains/' '{split($2,tmp,/\//); print tmp[1]}' <<<"$files")` – Akshay Hegde Oct 27 '17 at 03:16
  • I am okay with this post and I generaly use -exec on find for perform actions ; but it looks crappy on this subject. Except if I am doing it the wrong way. – Atille Oct 27 '17 at 03:31
  • I just edited the question, adding the second version I wrote. – Atille Oct 27 '17 at 04:06
  • There is no real need to read the files one by one. All matches could be read with a single read which will make the solution much more efficient. – codeforester Oct 27 '17 at 04:27
0

You can simplify your logic greatly and make it much more efficient and readable:

config_re='/wp_includes/version.php$'
while IFS=: read -r file match; do
    [[ $file =~ $config_re ]] || continue # skip if it is some other version.php

    # logic to extract the domain from file variable
    IFS=/ read -ra domain_arr <<< "$file"
    domain="${domain[2]}" # adjust the array index as needed

    # logic to extract version from matched part
    IFS='=' read -ra version_arr <<< "$match"
    version="${version_arr[1]}"
done < <(find /path/to/domains -type f -name version.php -exec grep '$wp_version' {} /dev/null \;)
  • find /path/to/domains -type f -name version.php -exec grep -H '$wp_version' {} \; extracts all the matches in one shot; grep -H ensures that full path of the version file (notice the full path in find's directory argument is prefixed to the match in the output

I haven't tested this code. I am willing to fix it in case you run into any issues.

codeforester
  • 39,467
  • 16
  • 112
  • 140
  • 1
    Thanks for your answer : I do not have enough experience in development for really understand that kind of syntax ; I used to work with really linear command lines. Will practice on this example. – Atille Oct 27 '17 at 04:29
  • Basically, the `find` command produces a comma separated output (/path/to/domain/file:$wp_version...) and read loop reads them into two variables `file` and `match`, with IFS set to `:` which is the input delimiter. Inside the loop, we extract the domain from full path of the file and version from the match. That's all. – codeforester Oct 27 '17 at 04:31
  • You can refer to this post to understand how `read` works in Bash: https://stackoverflow.com/a/41646525/6862601 – codeforester Oct 27 '17 at 04:35
  • 1
    @codeforester I just read your answer, its nice, but what it does is it looks for all files, rather than just `version.php`, which probably make slow. – Akshay Hegde Oct 27 '17 at 07:40
  • That was an oversight @3161993. Modified the answer. Added extra safety make sure we only look at `wp_includes/versions.php` as well. – codeforester Oct 27 '17 at 13:25
0

I do not know what you intend by more elegant, which is a rather subjective criteria, but here are a few hints about making a more efficient use of some commands:

  1. find is more powerful than what you use. It can search using regular expressions (-regex), execute commands on the files that it finds (-exec action)... Examples:

    $ re1=".*/domains/[^/]*"
    $ find /home -type d -regex "$re1" -printf "%f : \n"
    domain.com : 
    another.com : 
    again.com : 
    
    $ re2="*/domains/*/wp-includes/version.php"
    $ re3="wp_version"
    $ find /home -type f -ipath "$re2" -exec grep "$re3" '{}' \;
    $wp_version = '1.1'; # for example
    $wp_version = '2.2'; # for example
    $wp_version = '3.3'; # for example
    

    The {} parameter of -exec is substituted the path of the found file. Note that -exec commands must be terminated by a semi-colon that must be escaped (\;) to prevent its interpretation by the shell. find can also combine several grouped (\(...\)) test-action expressions (-o, -a):

    $ find /home \( -type d -regex "$re1" -printf "%f : " \) -o \
        \( -type f -ipath "$re2" -exec grep "wp_version" '{}' \; \)
    domain.com : $wp_version = '1.1'; # for example
    another.com : $wp_version = '2.2'; # for example
    again.com : $wp_version = '3.3'; # for example
    
  2. grep too, if it is a recent GNU grep version, is more powerful than what you use. It has a partial support of perl regular expressions (-P option) and can output only the matching part (-o). Example:

    $ re3="\\\$wp_version\s*=\s*'\K\d+(\.\d+)*"
    $ grep -Po "$re3" version.php
    1.1
    

If we glue all this together, and use bash variables for better readability (infamous cheat to shorten the final command line), we can use the following:

$ re1=".*/domains/[^/]*"
$ re2="*/domains/*/wp-includes/version.php"
$ re3="\\\$wp_version\s*=\s*'\K\d+(\.\d+)*"
$ find /home \( -type d -regex "$re1" -printf "%f : " \) -o \
    \( -type f -ipath "$re2" -exec grep -Po "$re3" '{}' \; \)
domain.com : 1.1
another.com : 2.2
again.com : 3.3

But all this is very complicated. Moreover, it works only if there is one and only one version file per domain and it does not filter out the versions more recent than 4.8. A loop and a bit of bash black magic are probably much simpler, easier to understand and maintain, and easy to extend with the version filtering:

re1="*/domains/*/wp-includes/version.php"
re2="\\\$wp_version\s*=\s*'\K\d+(\.\d+)*"

while read -d $'\0' f; do
  [[ $f =~ .*/domains/([^/]+)/.* ]] && domain="${BASH_REMATCH[1]}"
  version=$( grep -Po "$re2" "$f" )
  [[ ! $version =~ 4\.8(\.\d+)* ]] && printf "%s : %s\n" "$domain" "$version"
done < <( find . -type f -ipath "$re1" -print0 )

Note the use of the \0 delimiter (instead of new lines) in the read (-d $'\0') and find (-print0) commands. This may help in case you have strange directory names.

Renaud Pacalet
  • 25,260
  • 3
  • 34
  • 51
  • By "more elegant", I was talking about a better use of some commands that would produce the same result as the long one. Thank for explanations and examples ; I'll try it at work and maybe implement this. Looks great and more optimized. – Atille Oct 27 '17 at 16:41