Click here to Skip to main content
15,878,945 members
Articles / Programming Languages / C#

Touch Utility for Windows

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
16 May 2019CPOL4 min read 10.7K   380   9   1
A utility program to update the last-accessed time and last-modified time attributes of files and directories

Introduction

Touch is a utility program in Unix-like systems used to change the last-accessed time and last-modified time attributes of files and directories. It creates the file (directory) if it does not exist. I usually use it to create empty files and directory structures for projects I am working on. This utility, unfortunately, doesn't come with Windows. So I created my own. I hope it will be useful to you too.

Using the Code

The following illustrates basic usage of the utility:

> touch afile.cs *.html

Change the last-accessed-time of afile.cs, and all files with .html extension in the current directory, to the current date and time. If afile.cs does not exist, it will be created.

> touch -m --date="Jan 1, 2017 9:15 am" workspace/afile.cs

Change the last-modified-time of afile.cs to the specified date and time. Create it, along with its path, if it does not exist.

> touch -c -a -m --reference=workspace/afile.cs workspace/project/

Change the last-accessed-time and last-modified-time of project directory, if it does exist, to the corresponding dates and times of afile.cs.

To view usage information, type: touch --help.

Usage: touch.exe [OPTION]... FILE...
Update the access and modification times of each FILE to the current time.

Mandatory arguments to long options are mandatory for short options too.
  -a                     change only the access time
  -c, --no-create        do not create any files
  -d, --date=STRING      parse STRING and use it instead of current time
  -f                     (ignored)
  -m                     change only the modification time
  -r, --reference=FILE   use this file's times instead of current time
  -t STAMP               use [[CC]YY]MMDDhhmm[.ss] instead of current time
  --time=WORD            change the specified time:
                           WORD is access, atime, or use: equivalent to -a
                           WORD is modify or mtime: equivalent to -m
      --help     display this help and exit
      --version  output version information and exit

Note that the -d and -t options accept different time-date formats.

If a FILE is -, touch standard output.

Describing the Code

The application consists of two classes: The CommandLineParser class, used to process the command line arguments and validate the options provided, and the Program class, which uses those options to properly update dates and times.

To compile:

> csc /out:touch.exe CommandLineParser.cs Program.cs

The Command Line Parser

The CommandLineParser class is based on the class with the same name created by Mike Ellison to parse command lines having Microsoft-style options format (e.g. /t, /out:filename, ...). This class is a modified version, that uses the Unix-style options format (e.g. -t, --out=filename, ...), and adds options validation. Essentially, this class parses the command line arguments, and stores it in a Dictionary of arguments and values, for further processing. Optionally, you might include a list of valid options, and a list of the options that should be set by the user. This class then will validate the arguments against the provided options.

This is the class constructor:

C#
public CommandLineParser(string[] args, 
                         List<string> options = null, 
                         List<string> valuedOptions = null) {
    _argsDict = new Dictionary<string, string>();            
    int i = 0;
    while(i < args.Length) {
        string s = args[i];
        // if it is an option
        if (s.StartsWith("-") && (s.Length > 1)) {
            // if it is a long option
            if (s.StartsWith("--")) {
                int j = s.IndexOf("=");
                if (j == -1) {
                    if ((valuedOptions != null) && valuedOptions.Contains(s)) {
                        if ((i + 1) < args.Length) _argsDict.Add(s, args[++i]);
                        else throw new Exception("option " + s + " requires an argument");
                    }
                    else _argsDict.Add(s, "");
                }
                else {
                    string v = s.Substring(j + 1).Trim();
                    s = s.Substring(0, j).Trim();
                    _argsDict.Add(s, v);
                }
            }
            // if it is a short option
            else if (s.Length == 2) {
                if ((valuedOptions != null) && valuedOptions.Contains(s)) {
                    if ((i + 1) < args.Length) _argsDict.Add(s, args[++i]);
                    else throw new Exception("option " + s + " requires an argument");
                }
                else _argsDict.Add(s, "");
            }
            // if it is a flag of multiple options
            else {
                Slice(s, options, valuedOptions);
                i++;
                continue;                        
            }
            // is it a legitimate option?
            if ((options != null) && !options.Contains(s)) 
                throw new Exception("invalid option " + s);
            // does it have a value when it should not?
            if ((options != null) && (valuedOptions != null) 
                && !valuedOptions.Contains(s) && (GetValue(s) != "")) 
                    throw new Exception("option " + s + " should have no arguments");
        }
        // if it is not an option then it is a filename
        else _argsDict.Add(s, "<FILE>");
        i++;
    }
}

As you can see, the class stores the arguments and their values in a Dictionary. The keys can be accessed using the Arguments property:

public ICollection<string> Arguments {
    get { return _argsDict.Keys; }
}

Then we can iterate through the collection of keys, and retrieve the values using the GetValue() method. The Slash() method is there to accommodate a short-cut use by combining multiple options in one flag. It allows the user to write as part of the command line arguments, for example, -abc, which means -a -b -c or -mofilename to mean -m -o filename.

private void Slice(string s, List<string> options, List<string> vOptions) {
    if (options == null) {
        for (int i = 1; i < s.Length; i++) 
            _argsDict.Add("-" + s.Substring(i, 1), "");
        return;
    }
    string v = s.Substring(2).Trim();
    s = s.Substring(0, 2).Trim();
    if ((vOptions != null) && (vOptions.Contains(s)) ||
        ((options != null) && (!options.Contains("-" + v.Substring(0, 1)))))
        _argsDict.Add(s, v);
    else {
        _argsDict.Add(s, "");
        Slice("-" + v, options, vOptions);
    }
}

This class is built as an independent component. You can use it in your own program, if you want to capture options from, or have options set via, the command line, which is the case for most utility programs. In the next section, we will see how to do just that.

The Main Program

The program uses the CommandLineParser to process the command line flags, and use the resulting dictionary of options and values to determine two variables:

  • mode, with possible values: accessOnly (update the file’s last-accessed attribute), modifyOnly (update the file's last-modified attribute), or both. The default is accessOnly.
  • dateTime, the actual date and time to be used in updating the files' attributes. There are three possible sources for the value dateTime: from time-stamp (provided in the command line through the flag -t), from traditional date format (through flags -d or --date), or by specifying an existing file (using -r or --reference flags), and the needed date and time will be copied from the file's attributes. The default is DateTime.Now, which is the computer clock.

The main method first defines a list of allowed flags The program uses to set the various options and then handles simple requests for help and version.

static void Main(string[] args) {
    List<string> options = new List<string> { 
        "--no_create", "--date", "--time", "--reference", "--help", "--ver",
        "--version", "--h", "-a", "-c", "-d", "-f", "-m", "-r", "-t"
    };
    List<string> valuedOptions = new List<string> { 
        "--date",  "--time", "--reference", "-d", "-t", "-r" 
    };
    List<string> accessMode = new List<string> { "access", "atime", "use" };
    List<string> modifyMode = new List<string> { "modify", "mtime" };
    Mode mode = Mode.nothing;
    TimeSetter timeSetter = TimeSetter.now;
    string reference = null;
    bool refIsDir = false;
    DateTime dateTime = DateTime.Now;
    // if asked for 'help' or 'version', just print the message and exit
    foreach(string s in args) {
        if(s == "--help" || s == "--h") {
            Console.WriteLine(helpMsg);
            return;
        }
        if (s == "--version" || s == "--ver") {
            Console.WriteLine(versionMsg);
            return;
        }
    }  

Next, in the main processing block, the dateTime and mode variables are set, and the Touch() method is called for every filename in the command line:

C#
try {
    CommandLineParser parser = new CommandLineParser(args, options, valuedOptions);
    foreach (string s in parser.Arguments) {
        switch (s) {
            case "-t":
                // use time-stamp format
                // eg. 201702142200.00 which means 14 feb. 2017 10:00 pm  
                if(timeSetter != TimeSetter.now)
                    throw new Exception("cannot specify times from more than one source");
                timeSetter = TimeSetter.stamp;
                dateTime = GetDateTime(parser.GetValue(s));
                break;
            case "-d": 
            case "--date": 
                // use date format
                // eg. "May 20, 1999 8:35 AM", 2016/12/25, "2 October", ..etc.
                if(timeSetter != TimeSetter.now)
                    throw new Exception("cannot specify times from more than one source");
                timeSetter = TimeSetter.date;
                try { 
                    dateTime = DateTime.Parse(parser.GetValue(s)); 
                } catch (Exception) { 
                    throw new Exception("invalid date format '" + parser.GetValue(s) + "'");
                }
                break;
            case "-r": 
            case "--reference":
                // use the time of an existing reference file
                if (timeSetter != TimeSetter.now) throw new Exception(
                        "cannot specify times from more than one source"
                );
                timeSetter = TimeSetter.fromFile;
                reference = parser.GetValue(s);
                if (!File.Exists(reference)) {
                    if (!Directory.Exists(reference))
                        throw new Exception("reference '" + 
                              reference + "': No such file or directory");
                    else refIsDir = true;
                }
                break;
            default:
                // Check what to update: access time, modification time or both. 
                // if no flags provided, only the access time will be updated.
                if(s == "-a" || (s == "--time" && 
                            accessMode.Contains(parser.GetValue(s)))) {
                    if (mode == Mode.modifyOnly) mode = Mode.both;
                    else if (mode == Mode.nothing) mode = Mode.accessOnly;
                }
                if (s == "-m" || (s == "--time" && 
                            modifyMode.Contains(parser.GetValue(s)))) {
                    if (mode == Mode.accessOnly) mode = Mode.both;
                    else if (mode == Mode.nothing) mode = Mode.modifyOnly;
                }
                if(s == "--time" && 
                    !accessMode.Contains(s) && 
                        !modifyMode.Contains(parser.GetValue(s)))
                    throw new Exception(parser.GetValue(s) + 
                          " is not valid argument for --time");
                break;
        }                                    
    }
    // Check if we actually have any files or directories to update
    if (!parser.HasKey("-") && !parser.HasValue("<FILE>")) 
        throw new Exception("missing file operand");
    // setting times for STDOUT? Sorry. Not at the moment.
    if (parser.HasKey("-")) {
        Console.WriteLine("touch: setting times of '-': Function not implemented");
        return;
    }
    // All clear: We are out of excuses. Let's go and 'touch' some files
    foreach (string s in parser.Arguments) {
        if (parser.GetValue(s) == "<FILE>") {
            bool create = !parser.HasKey("--no_create") && !parser.HasKey("-c");
            try {                            
                Touch(s, mode, dateTime, reference, refIsDir, create);
            } catch (Exception ex) {
                // Access denied, ... etc.

                Console.WriteLine(ex.Message);
            }
        }
    }
} catch (Exception ex) {
    Console.WriteLine("touch: " + ex.Message);
    Console.WriteLine("Try 'touch --help' for more information");
}

GetDateTime() is a method that takes a time-stamp (in the form [[CC]YY]MMddhhmm[.ss]) and converts it to .NET DateTime class.

C#
static DateTime GetDateTime(string timeStamp) {
    string format = timeStamp;
    int year, month, day, hour, minute, second;
    try {
        // Throws an exception if it is not a number
        Double.Parse(timeStamp);
        // Remember the format is: [[CC]YY]MMDDhhmm[.ss] 
        if ((format.Length == 15) || format.Length == 12) {
            year = int.Parse(format.Substring(0, 4));
            format = format.Substring(4);
        }
        else if ((format.Length == 13) || format.Length == 10) {
            year = int.Parse("20" + format.Substring(0, 2));
            if (year > DateTime.Now.Year) year -= 100;
            format = format.Substring(2);
        }
        else year = DateTime.Now.Year;
        if (format.Length == 11 || format.Length == 8) {
            month = int.Parse(format.Substring(0, 2));
            day = int.Parse(format.Substring(2, 2));
            hour = int.Parse(format.Substring(4, 2));
            minute = int.Parse(format.Substring(6, 2));
            format = format.Substring(8);
            if (format.Length == 0) second = 0;
            else if (format.StartsWith("."))
                second = int.Parse(format.Substring(1, 2));
            else throw new Exception();
        }
        else throw new Exception();
        return new DateTime(year, month, day, hour, minute, second);
    }
    catch (Exception) { throw new Exception("invalid date format '" + timeStamp + "'"); }
}

The Touch method is where the actual updating/creation of files occur. First, we break the path into a directory part and a file part, check the existence of each:

C#
static void Touch(string path, Mode mode, DateTime dateTime, 
                  string fileRef = null, bool refIsDir = false, bool create = true) {
    string baseDir, pattern;
    baseDir = Path.GetDirectoryName(path);
    if (baseDir == "") baseDir = ".";
    pattern = Path.GetFileName(path);
    // The directory part does not exist. Create it?
    if (!Directory.Exists(baseDir)) {
        if (create) Directory.CreateDirectory(baseDir);
        else return;
    }
    // The file part does not exist. Create it?
    if (!File.Exists(path) && !Directory.Exists(path) &&
        (!(path.Contains("*") || path.Contains("?"))) && pattern != "") {
            if (create) File.Create(path).Close();
            else return;
    }
    if (pattern == "") {
        pattern = baseDir.Substring(baseDir.LastIndexOf("\\") + 1); 
        baseDir = Directory.GetParent(baseDir).ToString();
    }

Finally, we update the desired attributes of the directories and files.

C#
// Update the directories
foreach (string p in Directory.GetDirectories(baseDir, pattern)) {
    switch (mode) {
        case Mode.accessOnly:
            if (fileRef != null) dateTime = refIsDir? Directory.GetLastAccessTime(fileRef)
                                                    : File.GetLastAccessTime(fileRef);
            Directory.SetLastAccessTime(p, dateTime);
            break;
        case Mode.modifyOnly:
            if (fileRef != null) dateTime = refIsDir? Directory.GetLastAccessTime(fileRef) 
                                                    : File.GetLastWriteTime(fileRef);
            Directory.SetLastWriteTime(p, dateTime);
            break;
        default:
            if (fileRef != null) {
                Directory.SetLastAccessTime(p, refIsDir? Directory.GetLastAccessTime(fileRef) 
                                                       : File.GetLastAccessTime(fileRef));
                Directory.SetLastWriteTime(p, refIsDir? Directory.GetLastAccessTime(fileRef) 
                                                      : File.GetLastWriteTime(fileRef));
            }
            else {
                Directory.SetLastAccessTime(p, dateTime);
                Directory.SetLastWriteTime(p, dateTime);
            }
            break;
    }
}
// Update the files
foreach (string p in Directory.GetFiles(baseDir, pattern)) {
    switch (mode) {
        case Mode.accessOnly:
            if (fileRef != null) dateTime = refIsDir? Directory.GetLastAccessTime(fileRef) 
                                                    : File.GetLastAccessTime(fileRef);
            File.SetLastAccessTime(p, dateTime);
            break;
        case Mode.modifyOnly:
            if (fileRef != null) dateTime = refIsDir? Directory.GetLastAccessTime(fileRef) 
                                                    : File.GetLastWriteTime(fileRef);
            File.SetLastWriteTime(p, dateTime);
            break;
        default:
            if (fileRef != null) {
                File.SetLastAccessTime(p, File.GetLastAccessTime(fileRef));
                File.SetLastWriteTime(p, File.GetLastWriteTime(fileRef));
            }
            else {
                File.SetLastAccessTime(p, dateTime);
                File.SetLastWriteTime(p, dateTime);
            }
            break;
    }
}

That's all folks. Enjoy!

History

  • 15th May, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Eritrea Eritrea
Saleh Ali is an engineer by training and a programmer by disposition.

Comments and Discussions

 
Generaltouch and other apps from linux Pin
nolin1117-May-19 6:03
nolin1117-May-19 6:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.