2016-12-17, 2016-12-22 第12, 13回目 応用2
本日のテーマ
これからの2週は,cp
コマンドのプログラムを作成する時間とします.
以下のステップに従って作成してください.
ただし,ステップ2のプログラムは,ステップ1を含んでいなければいけません. 同じように,ステップ6はステップ1〜5の全てが実行できなければいけません.
提出方法について
- 各ステップに必要なファイルを1つのディレクトリにまとめてください.
- ディレクトリ名は自分の学生証番号としてください.
- 自分の学生証番号のディレクトリを zip で圧縮してください.
- 自分の学生証番号が
111111
の場合,展開すると次のようになっているようにしてください.-
$ ls 111111.zip $ unzip 111111.zip $ ls 111111.zip 111111 $ tree 111111 111111 ├── Arguments.java ├── Copy1.java ├── Copy2.java ├── Copy3.java ├── Copy4.java ├── Copy5.java ├── Copy6.java └── SimpleConsole.java
-
cpコマンドの実装
1. ファイルをコピーする.
UNIXのcp
コマンドのように,java Copy1 from_file to_file
を
実行すると,from_file
の内容がto_file
にコピーされるようなコマンドCopy1
を
作成してください.to_file
が存在している場合は上書きしてください
(単純にFileReader
で書き出せば上書きすることになります).
コマンドライン引数に必要な数のファイルが指定されない場合,エラーメッセージを出力しましょう. コマンドライン引数で指定されたものは,ファイルであるとしても構いません.
ヒント
// 必要な import 文を書いてください.
public class Copy1{
void run(String[] args) throws IOException{
// コマンドライン引数に必要な文のファイルが指定されているか確認する.
// args.lengthが2より小さい場合,必要な引数が指定されていない旨を出力して終了する.
// argsの0番目,1番目の要素からそれぞれFile型の実体を作成する.
// copyメソッドを呼び出す.
}
void copy(File from, File to) throws IOException{
// fromをBufferedReader(FileReader)で開く.
// toをPrintWriter(FileWriter)で開く.
// fromから読み込んだ内容をtoに書き出す.
// ファイルの終わりまでこの処理を繰り返す.
// from, to から開いたストリームを閉じる.
}
// mainメソッドは省略.
}
実行例
$ echo 'abcdef' > file1
$ ls file2
ls: file2: No such file or directory
$ java Copy1 file1 file2
$ cat file2
abcdef
$ java Copy1 file2
cp: コマンドライン引数には,少なくとも,コピー元,コピー先を指定する必要があります.
参考
2. 複数ファイルのディレクトリへコピーする
1. ファイルをコピーするでは,コマンドライン引数は2つ,かつ両方がファイルであることが前提でした.
ここでは,複数ファイルを一つのディレクトリにコピーするプログラムCopy2
を作成します.
java Copy2 from_file1 from_file2 from_file3 directory
を実行すると,from_file1
,from_file2
,from_file3
がディレクトリにコピーされます.
すなわち,directory/from_file1
,directory/from_file2
,directory/from_file3
が作成されます.
このような挙動を行うプログラムを作成してください.
まず,directory/from_file1
に書き込むためには,directory/from_file1
を表すFile
型の実体を作成しなければいけません.
そのためにまず,File
型の変数to
が出力先ディレクトリであるdirectory
を表し,
出力先のファイル名であるfrom_file1
を File
型の変数from
が表していると仮定します.
このとき,new File(to, from.getName())
という新たなFile
の実体を
作成することで,directory/from_file1
を表すFile
型の実体を作成できます.
コマンドライン引数の一番最後の要素がディレクトリであるか否かを判定してください. 一番最後の要素が存在し,ディレクトリである場合のみ,その前のコマンドライン引数が示すファイルの内容をディレクトリ以下にコピーしてください.
コマンドライン引数の一番最後の要素が存在しない場合,もしくは,ファイルであった場合は, 複数ファイルのコピーは行えませんので,エラーメッセージを出力し,終了してください.
ヒント
void run(String[] args){
// コマンドライン引数に必要な文のファイルが指定されているか確認する.
// 出力先は,コマンドライン引数の一番最後の要素である.
File to = new File(args[args.length - 1]);
if(.....){ // toが存在しない,もしくはファイルの場合.
// args.length が 2 より大きい場合,
// 複数ファイルを1つのファイルにコピーできない旨を出力し,終了する.
// そうでない場合,copyメソッドを呼び出す.
}
else if(....){ // toがディレクトリの場合.
for(....){ // argsを必要なだけ繰り返す.一番後ろの要素は省くことを忘れない.
File from = new File(arg[i]);
File toFile = new File(to, from.getName());
// fromをtoFileにコピーするよう copy メソッドを呼び出す.
}
}
}
実行例
$ for i in 1 2 3; do echo "file$i" > "file$i.txt" ; done
$ ls file?.txt
file1.txt file2.txt file3.txt
$ mkdir dir
$ java Copy2 file1.txt file2.txt dir
$ ls dir
file1.txt file2.txt
$ ls hoge
ls: hoge: No such file or directory
$ java Copy2 file3.txt file4.txt // -> file3.txt の内容を file4.txt にコピーしている.
$ cat file4.txt
file3
$ java Copy2 file2.txt file3.txt file5.txt
cp: 複数ファイルを一つのファイルにコピーできません.
参考
3. オプション解析を行う
オプション解析を行うようにプログラムを改良しCopy3
を作成してください.
オプションとは,コマンドに渡す -h
などの-
から始まる文字(文字列)です.
オプションによってプログラムの挙動が少し変わります.
最終的に 6. ディレクトリを再帰的にコピーするを終えると, 次のようなオプションを含んだプログラムが完成することになります.
$ java Copy3 -h
Usage: java Copy3 [OPTIONS] from_file to_file
java Copy3 [OPTIONS] from_file ... to_directory
OPTIONS
-h: このメッセージを表示して終了する(help).
-i: コピー先のファイルが存在していた時,ユーザに上書き確認を求める(interactive).
-r: ディレクトリを再帰的にコピーする(recursive).
-u: コピー先のファイルが存在しない場合,もしくはコピー元のファイルの方が新しい場合のみコピーする(update).
-v: 実行内容を表示する(verbose).
ヒント1. オプション解析
オプション解析を行うために,Arguments.java
を作成してください.Arguments
には,Boolean
型の5つの変数,help
,interactive
,recursive
,update
,
そして,verbose
と,ArrayList<String>
型のlist
の6つのフィールドを
持たせてください.list
は宣言時に初期化を行うようにしてください(new
を使いArrayList<String>
の
実体を作成し,代入しておいてください).Boolean
型の変数も
全て false
で初期化しておいてください.
次の parse
メソッドを Arguments
に定義してください.
void parse(String[] args){
for(String arg: args){
if(Objects.equals(arg, "-h")){
this.help = true;
}
else if(Objects.equals(arg, "-i")){
this.interactive = true;
}
else if(Objects.equals(arg, "-r")){
this.recursive = true;
}
else if(Objects.equals(arg, "-u")){
this.update = true;
}
else if(Objects.equals(arg, "-v")){
this.verbose = true;
}
else{
this.list.add(arg);
}
}
}
ヒント2. オプション解析とヘルプの表示
次に,Copy3
クラスのrun
メソッドを次のように定義しましょう.
void run(String[] args){
Arguments arguments = new Arguments();
arguments.parse(args);
if(arguments.help){
this.printHelp();
}
else{
this.performCopy(arguments);
}
}
このメソッドのでは,-h
オプションが指定された時,printHelp
メソッドを呼び出すようになっています.
上記に示したヘルプメッセージを表示するようprintHelp
メソッドの処理を書いてください.
単純に上に示したメッセージをSystem.out.println
で出力すれば良いでしょう.
また,performCopy
はステップ2までの処理(run
)を基本として
書き直してください.String
型の配列であるargs
ではなく,ArrayList<String>
から
要素を取り出す必要があります.
ヒント3. verboseオプションが指定された場合の処理
また,-v
オプション(verbose)が指定された場合,どのファイルがどこにコピーされたのかを表示するようにしてください.
次のような処理になるでしょう.「コピー処理」部分はfrom
からto
へのコピーの処理に置き換えてください.
void copy(File from, File to, Arguments args) throws IOException{
// コピー処理
// ...
if(args.verbose){
System.out.printf("%s -> %s%n", from.getPath(), to.getPath());
}
}
実行例
$ for i in 1 2 3 ; do echo "file$i" > "file$i.txt" ; done
$ java Copy3 -v file1.txt hoge.txt
file1.txt -> hoge.txt
$ mkdir dir2
$ java Copy3 -v file1.txt file2.txt file3.txt dir2
file1.txt -> dir2/file1.txt
file2.txt -> dir2/file2.txt
file3.txt -> dir2/file3.txt
参考
4. ファイルの上書きを確認する.
ファイルの上書き確認を行うようプログラムを改良し,Copy4
を作成しましょう.
コピー先のファイルが存在し,ディレクトリでなく通常のファイルの場合(to.exists() && to.isFile()
)
ユーザに上書きして良いかの確認を取りましょう.
ヒント
このプログラムを実現するには,copy
メソッドの最初に,isOverwrite
メソッドを呼び出し,
上書きするかを判定します.isOverwrite
メソッドでは,まずチェックすべきかどうか,すなわち,
出力先が存在しており,かつ,ファイルであるかを確認しています.
その上で,チェックすべきである場合は,interactive
オプションが指定されており,
ユーザが上書きしない場合に,isOverwrite
は false
を返すようにしています.
なお,ユーザへの確認処理はisOverwriteAskToUser
メソッドで行っています.
なお,SimpleConsole.java
は第9回目 練習問題2. 電話帳の作成と同じものを利用してください.
SimpleConsole.java
からダウンロードできます.
また,実際にコピーを行う処理をcopy
メソッド内からdoCopy
メソッドに移しています.
Boolean isOverwriteAskToUser(File to) throws IOException{
SimpleConsole console = new SimpleConsole();
while(true){
System.out.printf("%sを上書きしますか? (y/n [n]) ", to.getName());
String line = console.readLine().trim();
if(Objects.equals(line, "y")){
return true;
}
else if(Objects.equals(line, "") || Objects.equals(line, "n")){
System.out.println("上書きしません.");
return false;
}
else{
System.out.println("yかnを入力してください.");
}
}
}
Boolean isOverwrite(File to, Arguments args) throws IOException{
if(to.exists() && to.isFile()){
if(args.interactive && !isOverwriteAskToUser(to)){
return false;
}
}
return true;
}
void copy(File from, File to, Arguments args) throws IOException{
if(isOverwrite(to, args)){
this.doCopy(from, to);
// verboseの処理
}
}
void doCopy(File from, File to){
// コピー処理
// ...
}
実行例
$ for i in 1 2 3 ; do echo "file$i" > "file$i.txt" ; done
$ java Copy4 file1.txt file2.txt
$ cat file2.txt
file1
$ java Copy4 -i file1.txt file3.txt
file3.txtを上書きしますか? (y/n [n]) aaaa // aaaa を入力した.
yかnを入力してください.
file3.txtを上書きしますか? (y/n [n]) n // nを入力した.
上書きしません.
$ cat file3.txt
file3
$ java Copy4 -i -v file1.txt file3.txt
file3.txtを上書きしますか? (y/n [n]) y // yを入力した.
file1.txt -> file3.txt
$ cat file3.txt
file1
参考
5. 出力先のファイルが新しい場合はコピーしない
ファイルの更新日時を確認し,出力先のファイルの方が新しければコピーしないようにしましょう.Copy4
を
コピーしてCopy5
を作成し,処理を追加しましょう.
先ほど作成したisOverwrite
メソッドを修正して,この確認を行うようにしましょう.
ヒント
Boolean isOverwrite(File from, File to, Arguments args) throws IOException{
if(to.exists() && to.isFile()){
Boolean overwriteFlag = true; // デフォルトは上書きする.
if(args.update){ // updateオプションが指定された場合
// toの方が新しい場合,上書きしない.
overwriteFlag = ... // 上書きしない.
}
// 上書き,かつ,インタラクティブであれば,ユーザに上書きの可否を問い合わせる.
// ユーザが上書きを指示しなければ上書きしない.
if(overwriteFlag && args.interactive && !isOverwriteAskToUser(to)){
overwriteFlag = false;
}
return overwriteFlag;
}
return true;
}
なお,Copy4
で作成したisOverwrite
メソッドに新しい条件を追加しています.
条件が複数になったため,overwriteFlag
というBoolean
型の変数で上書き確認を行なっています.
実行例
$ for i in 1 2 3 ; do echo "file$i" > "file$i.txt" ; done
$ touch file1.txt // file1.txt の最終更新日時を更新した.
$ java Copy5 -u -v file1.txt file3.txt // file3.txt の方が古いため,コピーする.
file1.txt -> file3.txt
$ touch file2.txt
$ java Copy5 -u -v file2.txt file1.txt // file1.txt の方が新しいため,コピーしない.
$ cat file2.txt
file2
$ java Copy5 -u -v -i file2.txt file1.txt // file1.txt の方が古いため,コピーする.
file1.txtを上書きしますか? (y/n [n]) n
上書きしません.
$ java Copy5 -u -v -i file2.txt file1.txt // file1.txt の方が古いため,コピーする.
file1.txtを上書きしますか? (y/n [n]) y
file2.txt -> file1.txt
$ cat file1.txt
file2
参考
6. ディレクトリを再帰的にコピーする
今までは,ファイルを出力先のファイル,もしくはディレクトリにコピーするコマンドを作成しました.
このステップでは,ディレクトリをコピーする処理を追加します.Copy5
をコピーし,Copy6
を作成してください.
ステップ2のヒントで作成したrun
メソッドでは,
出力先(to
)が,存在しない,もしくはファイルの場合と,出力先がディレクトリに場合に場合分けを行い,
それぞれで処理を実行していました.
ヒント
ディレクトリを再帰的にコピーするには,copyRecursive
メソッドを用意します.
そこで,ディレクトリ内のファイル,ディレクトリを調べ,ファイルであれば,
すでに作成済みであるcopy
メソッドを呼び出します.
ディレクトリであれば,copyRecursive
の再帰呼び出しを行います.
ここで,copy
メソッドを呼び出すとき,出力先となるFile
型の変数に気をつける必要があります.
例えば,from
にaaa
が指定され,その中のaaa/bbb/file.txt
をdest
にコピーするとき,単純に new File(to, file)
とすると,dest/aaa/bbb/file.txt
に出力されることになります.
本来の出力は,dest/bbb/file.txt
であるはずです.そのファイル名を取得しているのが,createToFile
です.
void performCopy(String[] args){
// コマンドライン引数に必要な文のファイルが指定されているか確認する.
// 出力先は,コマンドライン引数の一番最後の要素である.
File to = new File(args[args.length - 1]);
if(.....){ // toが存在しない,もしくはファイルの場合.
// args.length が 2 より大きい場合,
// 複数ファイルを1つのファイルにコピーできない旨を出力し,終了する.
// そうでない場合,fromがディレクトリか否かを調べる.
else{
File from = new File(arguments.list.get(0));
if(from.isDirectory()){ // from がディレクトリの場合.
// toがファイルの場合
System.out.println("cp: ディレクトリをファイルにコピーできません.");
// コマンドライン引数に recursive が指定された場合.
// copyRecursiveメソッドを呼び出す.
this.copyRecursive(from, from, to, arguments);
else{ // その他の場合
System.out.printf("cp: %sはディレクトリです(コピーしません)%n", from.getName());
}
}
else{ // fromがファイルの場合.
this.copy(from, to, arguments);
}
}
}
else if(....){ // toがディレクトリの場合.
for(....){ // argsを必要なだけ繰り返す.一番後ろの要素は省くことを忘れない.
File from = new File(arg[i]);
// fromがファイルの場合
File toFile = new File(to, from.getName());
// copyメソッドを呼び出し,コピーする.
// コマンドライン引数に recursive が指定された場合.
// copyRecursiveメソッドを呼び出す.
this.copyRecursive(from, from, to, args);
else{ // その他の場合
System.out.printf("cp: %sはディレクトリです(コピーしません)%n", from.getName());
}
}
}
}
....
void copyRecursive(File base, File from, File to, Arguments args) throws IOException{
for(File file: from.listFiles()){
if(file.isDirectory()){
// copyRecursiveメソッドを再帰呼び出し.
}
else{
File toFile = this.createToFile(base, file, to);
// toFile の親ディレクトリ(File型)を取得する(toFile.getParentFile()).
// toFile の親ディレクトリが存在しない場合作成する(parent.mkdirs()).
this.copy(file, toFile, args);
}
}
}
File createToFile(File base, File from, File to){
String basePath = base.getPath();
String fromPath = from.getPath();
String newPath = fromPath.substring(basePath.length() + 1);
return new File(to, newPath);
}
実行例
$ mkdir dir1
$ for i in 1 2 3 ; do echo "file $i in dir1" > dir1/file$i.txt ; done
$ ls dir1
file1.txt file2.txt file3.txt
$ ls dir2
ls: dir2: No such file or directory
$ java Copy6 dir1 dir2
cp: dir1はディレクトリです(コピーしません)
$ java Copy6 -r -v dir1 dir2
dir1/file1.txt -> dir2/file1.txt
dir1/file2.txt -> dir2/file2.txt
dir1/file3.txt -> dir2/file3.txt