- 私のF# → しごとちゅうに書いた小さなコード晒す
- neno
- 2016/6/28 #nagoyakaeru
exeファイルとかdllファイルを右クリック→プロパティ→詳細情報で見れる「ファイルバージョン」とかを一括で取得したい!という要望にこたえる。
.NET1.1しかインストールされてない環境でも動きます。そういう要件だったのです。 C# のコンソールアプリケーションです。
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace get_versions
{
/// <summary>
/// プログラム本体です
/// </summary>
class Program
{
const string howToUse = "Usage: get_versions path\\to\\root\\dir\nList file version information in directory and sub directories recursively.";
/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine(howToUse);
return;
}
string rootDirPath = args[0];
if (!Directory.Exists(rootDirPath))
{
Console.WriteLine("1st argument is not a directory path.");
return;
}
DirectoryInfo directory = new DirectoryInfo(rootDirPath);
PrintFiles(directory);
}
/// <summary>
/// ディレクトリツリーを走査しながらファイル情報を出力します
/// </summary>
/// <param name="current">現在のディレクトリの情報</param>
static void PrintFiles(DirectoryInfo current)
{
foreach (DirectoryInfo d in current.GetDirectories())
{
PrintFiles(d);
}
foreach (FileInfo f in current.GetFiles())
{
if (!IsTargetFile(f))
{
continue;
}
FileVersionInfo v = FileVersionInfo.GetVersionInfo(f.FullName);
Console.WriteLine(GetRow(f, v));
}
}
static readonly string[] targetExtensions = { ".exe", ".dll" };
/// <summary>
/// 出力対象のファイルかどうか確認します
/// </summary>
/// <param name="f">ファイル情報</param>
/// <returns>trueなら出力対象</returns>
static bool IsTargetFile(FileInfo f)
{
foreach (string extension in targetExtensions)
{
if (f.Extension.ToLower() == extension)
{
return true;
}
}
return false;
}
const string separator = ",";
const string quote = "\"";
/// <summary>
/// ファイルとバージョンの情報を文字列化します
/// </summary>
/// <param name="file">ファイル情報</param>
/// <param name="version">バージョン情報</param>
/// <returns>出力形式の文字列</returns>
static string GetRow(FileInfo file, FileVersionInfo version)
{
StringBuilder sb = new StringBuilder();
sb.Append(quote);
sb.Append(file.DirectoryName);
sb.Append(quote);
sb.Append(separator);
sb.Append(quote);
sb.Append(file.Name);
sb.Append(quote);
sb.Append(separator);
sb.Append(quote);
sb.Append(version.FileVersion);
sb.Append(quote);
sb.Append(separator);
sb.Append(quote);
sb.Append(file.Length);
sb.Append(quote);
sb.Append(separator);
sb.Append(quote);
sb.Append(file.LastWriteTime);
sb.Append(quote);
return sb.ToString();
}
}
}
スプリント中のストーリーごとに、自分の使った工数と、チームの使った工数を調べたい!という思いにこたえる。
なおredmine環境については弊社環境前提のため、社外では多分いろいろ動きません。何かALMiniumとか入ってます。
エフシャープスクリプトなのでVisualStudioから動かします。コンソールアプリ化はめんどうなのでしていません。
// スプリントの自分の作業工数をストーリー別に集計します。
/// Redmine上のユーザーの表示名(苗字+半角スペース+名前)を設定します
let userName = "おなまえ"
/// APIを利用するためのキーです。http://れっどまいん/my/account 個人設定画面の「APIアクセスキー」から確認できます。
let apiKey = "hogefuga"
/// 対象プロジェクトを指定します
let projectId = "XXXXXX"
/// 対象バージョンを指定します。カンバンのURL(http://れっどまいん/rb/taskboards/****)の****部分です。
let fixedVersionId = "xxxx"
/// 出力先のファイル名を指定します
let distination = "result.txt"
/// Redmineの対象のストーリーチケットの一覧を取得するためのURLのフォーマットです
let issuesUrlFormat = "http://れっどまいん/projects/{0}/issues.xml?utf8=%E2%9C%93&set_filter=1&status_id=%2A&fixed_version_id={1}&backlogs_issue_type=story&page={2}"
/// Redmineの作業時間エントリを検索するためのURLのフォーマットです
let timeEntriesUrlFormat = "http://れっどまいん/projects/{0}/issues/{1}/time_entries.xml?page={2}"
// -----------------------------------------------------------------------------------------------
// 以下は取得処理です。
#r "System.Xml.Linq.dll"
#r "System.Net.Http.dll"
open System
open System.IO
open System.Text
open System.Xml.Linq
open System.Xml.XPath
open System.Xml
open System.Net.Http
let getResponse url =
use client = new HttpClient()
printfn "request: %s" url
let req = client.GetAsync url
if not req.Result.IsSuccessStatusCode then failwith "API Error."
else req.Result.Content.ReadAsStringAsync().Result |> XElement.Parse
let getPages getPage =
(Seq.unfold
(fun page ->
getPage page
|> Seq.toList
|> (function
| [] -> None
| xs -> (Some (xs, page + 1))
))
1)
type Issue = { IssueId:int; Subject:string; }
let getIssues () =
let getPage page =
let url = String.Format (issuesUrlFormat, projectId, fixedVersionId, page)
let url = url + "&key=" + apiKey
let response = getResponse url
response.Elements()
|> Seq.map (fun x ->
{
IssueId = x.Element("id" |> XName.op_Implicit).Value |> Int32.Parse;
Subject = x.Element("subject" |> XName.op_Implicit).Value
})
getPages getPage
|> Seq.collect id
type TimeEntry = { TimeEntryId:int; UserName:string; ActivityName:string; Hours:float; }
let getTimeEntries issue =
let getPage page =
let url = String.Format (timeEntriesUrlFormat, projectId, issue.IssueId, page)
let url = url + "&key=" + apiKey
let response = getResponse url
response.Elements()
|> Seq.map (fun x ->
{
TimeEntryId = x.Element("id" |> XName.op_Implicit).Value |> Int32.Parse;
UserName = x.Element("user" |> XName.op_Implicit).Attribute("name" |> XName.op_Implicit).Value;
ActivityName = x.Element("activity" |> XName.op_Implicit).Attribute("name" |> XName.op_Implicit).Value;
Hours = x.Element("hours" |> XName.op_Implicit).Value |> Double.Parse;
})
getPages getPage
|> Seq.collect id
|> (fun xs -> issue, List.ofSeq xs)
type StorySummary = { StoryId:int; Title: string; TotalHours:float; MyHours:float }
let toStorySummary ((issue: Issue), (timeEntries: TimeEntry list)) =
{
StoryId = issue.IssueId;
Title = issue.Subject;
TotalHours = timeEntries |> List.sumBy (fun x -> x.Hours)
MyHours = timeEntries |> List.filter (fun x -> x.UserName = userName) |> List.sumBy (fun x -> x.Hours)
}
let writeAllLine (path: string) (lines: string seq) =
use writer = new StreamWriter(path, false, Encoding.UTF8)
lines |> Seq.iter (fun l -> writer.WriteLine(l))
writer.Close()
let distnationFilePath = Path.Combine(__SOURCE_DIRECTORY__, distination)
getIssues ()
|> Seq.map getTimeEntries
|> Seq.toList
|> List.filter (fun (issue, timeEntries) ->
timeEntries
|> List.exists (fun x -> x.UserName = userName))
|> List.map toStorySummary
|> List.map (fun x -> sprintf "%d\t%s\t%A\t%A" x.StoryId x.Title x.TotalHours x.MyHours)
|> writeAllLine distnationFilePath
- 自動化しよう!
- 関数型言語べんりで書きやすいよ(F#!F#!)