I was investigating what it would take to make a standard unix script file that looks something like this:

#!drush php-script
<?php

drush_print("this is a simple script");

This works just fine, except for one prerequisite, one bug and one limitation:

Prerequisite: You must put $options['script-path'] = '/'; into your drushrc.php file in order to allow absolute paths to work in php-script.
Bug: When drush runs the line include($script_filename);, php will print the first "shebang" line ("#!drush php-script"). php-cli suppresses this automatically, but "include" does not. Using drush_op('system', 'php drush php-script file') does not work, though, because php will execute in a new environment in which Drupal is not bootstrapped. eval would have to be used after the # line is manually discarded.
Limitation: You must specify the target Drupal site with --root and --uri; it is not possible to use site aliases, because drush expects the site spec to appear before the command name.

I tend to think that writing "#!drush php-script" style php scripts would be useful and Unix-like, and all three of the issues above could be addressed with a bit of code. I'm not sure it's so much better than the existing drush command facility, but it might be a good way for Unix scripters to get a quick start into drush / Drupal php scripting, so maybe I'll pick this up again later.

Comments

moshe weitzman’s picture

Nifty. I never knew you could do custom shebang like that.

greg.1.anderson’s picture

Yeah; if you mark the file above executable and run it from bash, then bash will add the filename executed + the command-line args to the shebang line and execute it. So if you run the above as:

./mytest.php --arg=value

Then what runs is drush php-script ./mytest.php --arg=value. Could be useful to support this in drush someday.

greg.1.anderson’s picture

Hm, I just learned something new about shebang myself. It appears that on some (most?) systems, including Linux and Cygwin, parameters are not split. Ergo, the following:

File: testdrush.php
#!drush @site php-script

when executed as testdrush.php a b c, is equivalent to running drush "@site php-script" /path/to/testdrush.php a b c, as everything after the executable ("drush") is interpreted as the first parameter.

To further complicate things, the executable is supposed to always be a full path. "#!drush" works on Ubuntu, but it's not guaranteed to work on all platforms. The way to get around this is usually to use /usr/bin/env, like so:

#!/usr/bin/env drush php-script

However, we already know from the above that this will tell env to run "drush php-script", which will fail. :(

There is a solution... coming up.

greg.1.anderson’s picture

Status: Active » Needs work
StatusFileSize
new4.47 KB

Well, I don't quite have a solution to #3, but here is a work-in progress patch that solves the bug and prerequisite from #0, but ignores the fact that #!drush is not portable (you can use #!/path/to/drush php-script on platforms that do not support #!drush, though.

n.b. In this patch, the <?php in the script file is optional, but the shebang line must include "drush". I also coded up another very similar version where the <?php was required, but the shebang line did not require "drush". Not sure which is better.

Still thinking about the other stuff.

greg.1.anderson’s picture

Assigned: greg.1.anderson » Unassigned
Status: Needs work » Needs review
StatusFileSize
new17.77 KB

Success!

You might think from #3 that information has been lost, and there is
no good way to tell if execve has passed us space-separated arguments
(as opposed to the user quoting the arguement). However, with just
a little bit of introspection, drush can detect that it was launched
from a drush "shebang" script and fix everything up. See the comments
in the patch for more details.

This patch also adds the special '--bootstrap-to-first-arg' option.
This option checks to see if the first user-provided argument is an alias
or site specification; if it is, it will be shifted into the first arguement
position, where it will specify the site to bootstrap. The result of this
is that if your shebang line looks like this:

#!/path/to/drush @shift php-script

Then when you run that script, you can optionally provide an alias such
as @dev as the first argument (e.g. $ ./mydrushscript.php @dev scriptarg1
scriptarg2). Since this is the behavior that one would usually want,
it is default behavior for a canonical script. That is, a script
with a simple shebang line, like so:

#!/path/to/drush

will implicitly have "--bootstrap-to-first-arg" and "php-script" prepended, and will therefore
behave exactly like the first example. To write a script that does not
use --bootstrap-to-first-arg, then the drush command or at least one flag must be explicitly
included, like so:

#!/path/to/drush php-script

The usuall /usr/bin/env trick works for canonical scripts:

#!/usr/bin/env drush

However, it is *not* possible to add additional parameters after "drush"
when using this form. This is due to the limitation of execve, which
will pass drush and everything after it, including the spaces, as a single
argument to env, which in turn attempt to launch "drush php-script" (and fail).
/usr/bin/env is not necessary on some systems; Ubuntu Linux, for example,
allows:

#!drush php-script

This works exactly like the /usr/bin/env variant. Some O.S.s require the
script parameter to be an absolute path, though, so for better flexibility,
you might prefer to standardize on the location where you install drush
and just use the absoulte path to drush in your scripts.

Finally, this patch adds a new function, drush_shift(), which shifts
the next arguement of the arguement list and returns it. drush_get_arguements()
still returns an array that starts with the drush command (php-script)
and ths script being executed, and is followed by all of the user arguments.
drush_shift(), on the other hand, skips these first two arguements and
returns the first user argument the first time it is called.

Some examples:

testdrush.php

#!drush

drush_print("this is a simple script");
$self_record = drush_sitealias_get_record('@self');
_drush_sitealias_print_record($self_record);

drush_print("drush_get_arguments returns:\n" . implode("\n", drush_get_arguments()));
while ($arg = drush_shift()) {
  drush_print("one arg is " . $arg);
}

$ testdrush.php a b c

this is a simple script
$aliases[''] = array (
);
drush_get_arguments returns:
php-script
/home/ga/bin/testdrush.php
a
b
c
one arg is a
one arg is b
one arg is c

$ testdrush.php @gkdev a b c

this is a simple script
$aliases['greenknowe.org'] = array (
  'root' => '/srv/www/dev.greenknowe.org',
  'uri' => 'http://greenknowe.org',
);
drush_get_arguments returns:
php-script
/home/ga/bin/testdrush.php
a
b
c
one arg is a
one arg is b
one arg is c

I expect that this feature will be very popular with people who
are already very familiar with shell scripting, and should be
the perfect "gateway drug" to full drush commands. I also feel
that drush shebang scripts are better than drush commands
for some uses, such as samples in drush_extras that
need to be customized by the user. For example, #681690: Proposal: site-sync and other "site" commands for drush would
fall into this category. Scripts such as these could be placed in
drush_extras/scripts; users can copy these to their own work
folders to customize and use.

moshe weitzman’s picture

Status: Needs review » Needs work

Looks good.

I feel like we should ship with one of these scripts in examples directory. Perhaps another copy of make-me-a-sandwich. I guess we need to warn Windows users that this is not for them (Cygwin understands this?)

+++ commands/core/core.drush.inc	28 Oct 2010 21:53:18 -0000
@@ -657,50 +660,68 @@ function _drush_core_is_named_in_array($
+  drush_bootstrap_max();

Should this be in hook_COMMAND_init() like we did during recent bootstrap revamp?

+++ includes/command.inc	28 Oct 2010 21:53:20 -0000
@@ -65,13 +69,115 @@ function drush_parse_args() {
+ * Pop an arguement off of drush's arument list

no 'e' in argument. happens a few times ... typo: 'arument'.

+++ includes/command.inc	28 Oct 2010 21:53:20 -0000
@@ -65,13 +69,115 @@ function drush_parse_args() {
+ * Process the --bootstrap-to-first-arg option, if it is present.

lets copy some docs from this issue to this part of the code. explain why one uses this option.

luchochs’s picture

@greg.perseverancia.anderson:
Bravo!

greg.1.anderson’s picture

Status: Needs work » Needs review
StatusFileSize
new22.49 KB

Should this be in hook_COMMAND_init() like we did during recent bootstrap revamp?

Good call. This updated patch moves the bootstrap_max to the _init() phase. It does the same for other commands (status and core-cli) that also call bootstrap_max().

Fixed typos, enhanced docs, and added examples/helloworld.script. We could perhaps use a couple more examples, but this is a good start.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

Nice work. Minor comments below. Feel free to commit afterwards.

I'm wishing that this patch could add more code to script command and less to the includes. It is a bit invasive in that way. Perhaps some of the helper functions can move? If not, thats OK.

+++ examples/helloworld.script	30 Oct 2010 06:40:12 -0000
@@ -0,0 +1,77 @@
+// (although the later is not supported on all flavors

typo: latter instead of later

+++ examples/helloworld.script	30 Oct 2010 06:40:12 -0000
@@ -0,0 +1,77 @@
+// and the path to to this script.

repeated word

greg.1.anderson’s picture

Committed. Thanks for the review.

Unfortunately, that code which is in includes must be located there, because it is needed before drush has decided which command will executed. For example, it is possible to make your own custom shebang processor via "#!drush my-php-handler", for example, and process the script file however you wish. I could move the code to includes/shebang.inc, and only include it if needed, but you'd still need a few lines in includes to do that, and there isn't too much code that would go in shebang.inc, so I don't think it's quite worth it.

greg.1.anderson’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.