Skip to content

Instantly share code, notes, and snippets.

@nenono
Last active June 28, 2016 13:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nenono/a1581497c28c08f54d9d46788f17a2b1 to your computer and use it in GitHub Desktop.
Save nenono/a1581497c28c08f54d9d46788f17a2b1 to your computer and use it in GitHub Desktop.
  • 私のF# → しごとちゅうに書いた小さなコード晒す
  • neno
  • 2016/6/28 #nagoyakaeru

ファイルバージョンを一括で取得してcsvに吐くやつ

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につけた作業時間の記録を集計してcsvに吐くやつ

スプリント中のストーリーごとに、自分の使った工数と、チームの使った工数を調べたい!という思いにこたえる。

なお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#!)

板書

こちら

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment