1:- module( by_unix, [    
    2					(@)/1,
    3					(@@)/2,
    4					(@@)/3,
    5					which/2,
    6					cd/2,
    7					by_unix_retract/0,
    8					by_unix_assert/0,
    9					by_unix_version/2,
   10					by_unix_term_to_serial/2,
   11					op( 200, fy, @ ),
   12					op( 200, yfx, @@ ),
   13					op( 200, fy, -- ),
   14					op( 400, fx, / ),
   15					op( 400, fx, './' ),
   16   					op( 400, fx, '../' ),
   17					op( 600, yfx, '/../' )
   18				] ).   19
   20:- ensure_loaded( library(debug) ).

by_unix

An elegance layer to calling unix commands.

This library provides primitives that allow programmers and users to embed calls to process_create/3. The aim is to keep application code clear and succinct. This is achieved by (a) reducing the call to process_create/3 to its essential constituents and (b) allowing for term structures in the arguments of the call.

A simple example is

  ?- @ ls.

which lists all files by translating ls to process_create( path(ls), [], [] ).

To list all details (long list):

        ?- @ ls(-l).

which demonstrates translation of terms in arguments.

By_unix looks at the arguments of the terms right of an @ to decide which ones are options (3rd argument of process_create/3) assuming the rest to be arguments to the call (2nd argument of process_create/3). Command arguments can be terms which will be serialised, so that a/b becomes 'a/b'. The argument * is special and it is expanded fed into expand_file_name/2.

With SWI 7 you can now have '.' in atoms, making interactions with the OS even smoother.

?- @ mkdir( -p, /tmp/test_by_unix ).
?- @ cd( /tmp/test_by_unix ).
?- @ touch( empty.pl ).
?- @ rm( -f, empty.pl ).

?- @ cd( pack(by_unix) ).
?- Wc @@ wc( -l, pack.pl ).

Wc = ['10 pack.pl'].

?- @ cd( @ '$HOME' ).
?- [Pwd] @@ pwd.
Pwd = '/home/nicos'.

The main objective of by_unix is achieved by what has been described so far. We have found that more than 90 percent of the its uses are to produce elegant Goals that are clear to read and construct their arguments within Prolog. We provide some more features, which are described in what follows, but they should be seen as marginal.

Changing directory is not supported via cd in process_create as which cd fails to return an executable in bash. That is,

?- process_create( path(cd), ['/'], [] ).
ERROR: source_sink `path(cd)' does not exist

As oppose to:

?- [library(by_unix)].

?- @ cd( / ).
true.

?- @ ls.
bin   dev  home        initrd.img.old  lib64	   media  opt	root  sbin     srv  tmp  var
boot  etc  initrd.img  lib	       lost+found  mnt	  proc	run   selinux  sys  usr  vmlinuz
true.

?- @ cd( /home/nicos ).
?- @ cd( pack(by_unix) ).

which/2 provides a locator for executables. by_unix_term_to_serial/2 serialises Prolog terms to process_create/3 atoms, and by_unix_assert/0 allows for doing away with the @.

As process_create/3 quotes special characters, for instance

?- process_create( path(ls), ['*'], [] ).
/bin/ls: cannot access *: No such file or directory
ERROR: Process "/bin/ls": exit status: 2

By_unix allows for in-argument file name expansions via expand_file_name/2.

?- @ ls( -l, @('*.pl') ).

@@/2 provides dual functionality: either picking up output lines from the calling command or for maplist/2 applications. See @@/2 and @@/3.

A lines example

?- @ cd( @ '$HOME' ).
?- Pwd @@ pwd.
Pwd = ['/home/nicos'].

A maplist example

?- @ cd( pack(by_unix) ).
?- Files = ['by_unix.pl'], maplist( @@wc(-l), Files, Wcs ).
@author Nicos Angelopoulos
@version 0.1.6   2013/12/26
@see http://stoics.org.uk/~nicos/sware/by_unix

*/

 @@(-Lines, +Comm)
@@(+Comm, -Arg)
If first argument is a variable or list, it is interpretted to be the Lines invocation, Output from Comm instantiated in Lines. Attach Arg to Comm before processing as a Unix command.
  144@@(Lines,Goal) :-
  145	once( var(Lines); is_list(Lines) ),
  146	!,
  147	by_unix_separate( Goal, Name, TArgs, Opts ),
  148	\+ memberchk( stdout(_), Opts  ),
  149	unix_process( Name, Goal, TArgs, [stdout(pipe(Out))|Opts] ),
  150	read_lines(Out, Lines).
  151@@(Goal,Arg) :-
  152	by_unix_separate( Goal, Name, Args, Opts ),
  153	by_unix_term_to_serial( Arg, Serial ),
  154	to_list( Serial, Serials ),
  155	append( Args, Serials, All ),
  156	% process_create( path(Name), All, Opts ).
  157	unix_process( Name, Goal, All, Opts ).
  158% this is suitable for meta calls, with output
  159%% @@( +Comm, +Arg, -Lines ).
  160%
  161% Attach Arg to Comm before processing as a Unix command and provide output to Lines.
  162% Works with maplist/3 but Lines will be triply nested.
  163%
  164@@(Goal,Arg,Lines) :-
  165	by_unix_separate( Goal, Name, Args, Opts ),
  166	by_unix_term_to_serial( Arg, Serial ),
  167	to_list( Serial, Serials ),
  168	append( Args, Serials, All ),
  169	process_create( path(Name), All, [stdout(pipe(Out))|Opts] ),
  170	read_lines(Out, Lines ).
 @ +Goal
This is the main predicate of by_unix. See module documentation for examples.

For @cd( Arg ) see documentation of cd/2.

   ?- @ mkdir( -p, /tmp/test_by_unix ).
   ?- @ cd( /tmp/test_by_unix ).
   ?- @ touch( empty.pl ).
   ?- @ rm( -f, empty.pl ).
  185@(Goal) :-
  186	by_unix_separate( Goal, Name, TArgs, Opts ),
  187	unix_process( Name, Goal, TArgs, Opts ).
  188
  189by_unix_separate( Goal, Name, TArgs, Opts ) :-
  190	% Goal =.. [Name|GArgs],
  191	compound( Goal, Name, GArgs ),
  192
  193	which( Name, _Wch ), %fixme add error
  194	partition( pc_option, GArgs, Opts, Args ),
  195	maplist( by_unix_term_to_serial, Args, NesTArgs ),
  196	flatten( NesTArgs, TArgs ).
  197
  198unix_process( Cd, Goal, [_Arg], [] ) :-
  199	Cd == cd,
  200	!,
  201	arg( 1, Goal, Garg ),
  202	cd( Garg ).
  203unix_process( Name, _Goal, Args, Opts ) :-
  204	debug( by_unix, 'Sending, name: ~w, args: ~w, opts:~w.', [Name,Args,Opts] ),
  205	process_create( path(Name), Args, Opts ).
 which(+Which, -This)
Expand Which as a unix command in the path and return its absolute_file_name/3 in This. When Which is cd, variable This is also bound to cd. cd is handled separately as which cd fails in bash, as does process_create(path(cd), ['/'], [] ).

*/

  214which( Which , This ) :- 
  215	Which == cd,
  216	!, 
  217	This = cd.
  218which( Which, This ) :-
  219     absolute_file_name( path(Which), This,
  220			 [ extensions(['',exe]),
  221			   file_errors(fail),
  222			   access(exist) % shouldn't this be execute ?
  223			 ] ).  % does not succeed for built-ins !!!
  224
  225pc_option( Term ) :-
  226	compound( Term, Name, Args ),
  227	length( Args, 1 ),
  228	% functor( Term, Name, 1 ),
  229	known_pc_options( OptNames ),
  230	memberchk( Name, OptNames ).
 by_unix_term_to_serial(Term, Serial)
Term is serialised into an atomic Serial. When Term is *, Serial is the list of current files, while when Term = @ Atom, Serial is the result of applying expand_file_name( Atom, Serial ).
  237by_unix_term_to_serial( *, Files ) :-
  238	!,
  239	expand_file_name('*',Files).
  240by_unix_term_to_serial( @(Atom), Files ) :-
  241	atomic( Atom ),
  242	!,
  243	expand_file_name(Atom,Files).
  244
  245by_unix_term_to_serial( Term, Serial ) :-
  246	with_output_to( atom(Serial), write_term(Term,[quoted(false)]) ).
 by_unix_version(-Version, -Date)
Provides version and date of current release.
  252by_unix_version( 0:1:6, date(2013,12,26) ).
 by_unix_retract
Retract all user:goal_expansion(_,_).
  258by_unix_retract :-
  259	Head = user:goal_expansion(_,_),
  260	retractall( Head ).
 by_unix_assert
Allows for goal expansion of Com to @ Com, when Com is a which-able Unix command.
  ?- by_unix_asert.
  ?- mkdir( -p, /tmp/test_by_unix ).
  ?- cd( /tmp/test_by_unix ).
  ?- touch( empty.pl ).
  ?- ls( -l ).
  ?- rm( -f, empty.pl ).
  ?- ls( -a ).
  275by_unix_assert :-
  276	Head = user:goal_expansion(Term1,Term2),
  277	Body = (  (atomic(Term1) -> UnixCom = Term1
  278				; compound_name_arity(Term1,UnixCom,_Arity) ),
  279			which(UnixCom,_),
  280			Term2= @(Term1)
  281		  ),
  282	assert( (Head :- Body) ).
  283
  284known_pc_options( [stdin,stdout,stderr,cwd,env,process,detached,window] ).
 cd(-Old, +New)
Similar to working_directory/2, but in addition New can be a search path alias or the 1st argument of absolute_file_name/2. This is also the case when @ cd( New ) is called.
?- @ cd( pack ).
true.

?- @ pwd.
/usr/local/users/nicos/local/git/lib/swipl-7.1.4/pack
true.

?- @ cd( @ '$HOME' ).
true.

?- @ pwd.
/home/nicos
true.
  308cd( Old, New ) :-
  309	working_directory( Old, Old ),
  310	cd( New ).
  311
  312cd( Spec ) :-
  313	catch(absolute_file_name(Spec,Dir),_,fail),
  314	exists_directory( Dir ),
  315	!,
  316	working_directory( _, Dir ).
  317cd( Dir ) :-
  318	ground( Dir ),
  319	user:file_search_path( Dir, TermLoc ),
  320	expand_file_search_path( TermLoc, Loc ),
  321	exists_directory( Loc ),
  322	!,
  323	working_directory( _, Loc ).
  324cd( Atom ) :-
  325	atom( Atom ),
  326	!,
  327	working_directory( _, Atom ).
  328cd( Term ) :-
  329	compound( Term ),
  330	by_unix_term_to_serial( Term, Serial ),
  331	to_list( Serial, [Dir|_T] ), %fixme: warn if T \== [] 
  332	working_directory( _, Dir ).
  333
  334read_lines(Out, Lines) :-
  335        read_line_to_codes(Out, Line1),
  336        read_lines(Line1, Out, Lines).
  337read_lines(end_of_file, _, []) :- !.
  338read_lines(Codes, Out, [Line|Lines]) :-
  339        atom_codes(Line, Codes),
  340        read_line_to_codes(Out, Line2),
  341        read_lines(Line2, Out, Lines).
  342
  343compound( Term, Name, Args ) :-
  344	current_predicate( compound_name_arguments/3 ),
  345	compound( Term ),  % in real this is after the cut, here we are less strict we allows ls = ls()
  346	!,
  347	compound_name_arguments( Term, Name, Args ).
  348compound( Term, Name, Args ) :-
  349	Term =.. [Name|Args].
  350
  351to_list( Serial, Serials ) :-
  352	is_list( Serial ),
  353	!,
  354	Serial = Serials.
  355to_list( Serial, [Serial] )