Skip to content

Commit

Permalink
Add python3 support, improve readme and fix bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
joowani committed Jan 8, 2016
1 parent 2f32209 commit a98512f
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 207 deletions.
77 changes: 43 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
# dtags
# DTags
Directory tags for lazy programmers.
Written in Python, heavily inspired by [gr](https://github.com/mixu/gr)
Written in Python, inspired by [gr](https://github.com/mixu/gr)

## Introduction

Do you have a lot of [git](https://git-scm.com/) repositories or
[vagrant](https://www.vagrantup.com/) machines to manage? Does your daily workflow
require you to switch between directories all the time? Are you a lazy programmer?
If your answered *yes* to any of these questions, then dtags may be for you!
Do you have too many git repositories or vagrant machines to manage? Does your
daily workflow require you to switch between directories often? Are you always
looking for ways to type less? If your answered yes to any of these questions,
then dtags may be for you!

You can use dtags to tag any number of paths and run commands in them without ever
having to change directories. For example, you can tag a bunch of locations you
visit frequently like this:
```bash
tag ~/frontend ~/backend ~/tools ~/scripts @work
tag ~/vms/web ~/vms/db ~/vms/api @vms
```

Than do something cool like this:
```bash
run @work git status
run @vms vagrant up
run @work @vms ls -la
```
## Getting Started

## Installation

Install using [pip](https://pip.pypa.io) (supports version 2.7+ and 3.4+):
Install using [pip](https://pip.pypa.io) (Python 2.7+ and 3.4+ supported):
```python
sudo pip install --upgrade pip setuptools
sudo pip install --upgrade dtags
Expand All @@ -35,33 +20,57 @@ sudo pip install --upgrade dtags
Once installed, 4 commands will be available to you:
**tags**, **tag**, **untag** and **run**.

## Usage
You can tag any number of directories and run commands in them without ever
having to `cd`. For example, tag the directories you visit frequently:
```bash
> tag ~/frontend ~/backend ~/tools ~/scripts @project
> tag ~/vms/web ~/vms/db ~/vms/api @vms
```

Here are some ways I use dtags:
Then you can do run commands like this:
```bash
> run @project git status -sb
> run @vms vagrant up
```

You can even run the commands in parallel (but you lose font colors):
```bash
tag ~/frontend @frontend @work ~/backend @backend @work ~/tools @tools @work ~/scripts @scripts
> run -p @project git status -sb
> run -p @vms vagrant up
```

run @frontend @tools git pull
run @backend git checkout stage
run @backend git checkout
If you want an overview of all your tags, you can run the command `tags` to
display them in a variety of ways:
```bash
> tags @backend @frontend # filter by tags
> tags --json # display in json
> tags --expand # expand user home
> tags --reverse # show the reverse mapping
```

Lastly, to remove tags you don't need anymore:
```bash
> untag ~/frontend ~/backend ~/tools ~/scripts @project ~/backend @backend
> untag ~/vms/web ~/vms/db ~/vms/api @vms ~/has/many/tags @foo @bar
```

If you need more help you can always use the option `--help`.


## Auto-completion

dtags supports tab completion for zsh and bash.
I *strongly* recommend you to enable it.
Auto-completion for **zsh** and **bash** are supported. I *strongly* recommend
you to enable it so you don't have to type the `@` symbols all the time.

If you use **bash**, place the following lines in your **~/.bashrc** (or **~/.bash_profile** for OS X):
If you use **bash**, place the following lines in your **~/.bashrc**
(or **~/.bash_profile** for OS X):
```bash
eval "$(register-python-argcomplete run)"
eval "$(register-python-argcomplete tags)"
```

If you use **zsh**, place the following lines in your **~/.zshrc**:
```bash
# Enable bash autocompletion
```bash
autoload bashcompinit
bashcompinit
eval "$(register-python-argcomplete run)"
Expand Down
2 changes: 1 addition & 1 deletion dtags/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
GREEN = '\033[1;92m'
YELLOW = '\033[1;93m'
BLUE = '\033[1;94m'
MAGENTA = '\033[1;95m'
PINK = '\033[1;95m'
CYAN = '\033[1;96m'
CLEAR = '\033[0m'
24 changes: 11 additions & 13 deletions dtags/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,29 @@

from dtags.colors import *
from dtags.completion import ChoicesCompleter, autocomplete
from dtags.config import load_config
from dtags.config import load_tags
from dtags.help import HelpFormatter
from dtags.utils import expand_path

tmp_file = None
process = None
processes = []
command_description = """
Run commands in multiple directories.
dtags: run commands in multiple directories.
Targets can either by tag names or directory paths.
All tag names must be preceded by the {m}@{x} character.
For example, {y}run @a @b ~/foo ~/bar ls -la{x} will run:
e.g. running {y}run @a @b ~/foo ~/bar ls -la{x} will:
{y}ls -la{x} in all directories tagged {m}@a{x}
{y}ls -la{x} in all directories tagged {m}@b{x}
{y}ls -la{x} in directory {c}~/foo{x}
{y}ls -la{x} in directory {c}~/bar{x}
execute {y}ls -la{x} in all directories with tag {m}@a{x}
execute {y}ls -la{x} in all directories with tag {m}@b{x}
execute {y}ls -la{x} in directory {c}~/foo{x}
execute {y}ls -la{x} in directory {c}~/bar{x}
""".format(m=MAGENTA, c=CYAN, y=YELLOW, x=CLEAR)
""".format(m=PINK, c=CYAN, y=YELLOW, x=CLEAR)


def _print_header(tag, path):
"""Print the run information header."""
tail = " ({}{}{}):".format(MAGENTA, tag, CLEAR) if tag else ":"
tail = " ({}{}{}):".format(PINK, tag, CLEAR) if tag else ":"
print("{}{}{}{}".format(CYAN, path, tail, CLEAR))


Expand Down Expand Up @@ -86,7 +84,7 @@ def main():
signal.signal(signal.SIGTERM, _kill_signal_handler)
atexit.register(_cleanup_resources)

tag_to_paths = load_config()
tag_to_paths = load_tags()
parser = argparse.ArgumentParser(
prog="run",
description=command_description,
Expand Down Expand Up @@ -139,7 +137,7 @@ def main():
if parsed.parallel:
# Run the command in parallel
for path, tag in targets.items():
tmp_file = tempfile.TemporaryFile()
tmp_file = tempfile.TemporaryFile(mode='w+t')
process = subprocess.Popen(
command,
cwd=path,
Expand Down
130 changes: 55 additions & 75 deletions dtags/commands/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,111 +4,91 @@
from argparse import ArgumentParser

from dtags.help import HelpFormatter
from dtags.colors import MAGENTA, CYAN, YELLOW, CLEAR
from dtags.colors import PINK, CYAN, YELLOW, CLEAR
from dtags.chars import TAG_NAME_CHARS
from dtags.config import load_config, save_config
from dtags.config import load_tags, save_tags
from dtags.utils import expand_path, shrink_path

command_description = """
Tag specified directories.
cmd_description = """
dtags: tag directories.
All tag names must be preceded by the {m}@{x} character.
For example, after running {y}tag foo bar @a @b bar baz @c{x}:
e.g. after running {y}tag ~/foo ~/bar @a @b ~/bar ~/baz @c{x}:
directory {c}foo{x} will have tags {m}@a @b{x}
directory {c}bar{x} will have tags {m}@a @b @c{x}
directory {c}baz{x} will have tags {m}@c{x}
{c}~/foo{x} will have tags {m}@a @b{x}
{c}~/bar{x} will have tags {m}@a @b @c{x}
{c}~/baz{x} will have tags {m}@c{x}
""".format(m=MAGENTA, c=CYAN, y=YELLOW, x=CLEAR)
""".format(m=PINK, c=CYAN, y=YELLOW, x=CLEAR)

update_msg = "Added tag {m}{{}}{x} to directory {c}{{}}{x}".format(
m=MAGENTA, c=CYAN, x=CLEAR
)
msg = "Added tag {m}{{}}{x} to {c}{{}}{x}".format(m=PINK, c=CYAN, x=CLEAR)


def main():
tag_to_paths = load_config()
tag_to_paths = load_tags()
parser = ArgumentParser(
prog="tag",
usage="tag [paths] [tags] ...",
description=command_description,
usage="tag [[paths] [tags] ... ]",
description=cmd_description,
formatter_class=HelpFormatter,
)
parser.add_argument(
"-e", "--expand",
action="store_true",
help="Expand the directory paths"
)
parser.add_argument(
"arguments",
type=str,
nargs='+',
metavar='[paths] [tags]',
help="directory paths followed by tag names"
help="directory paths and tag names"
)
parsed = parser.parse_args()

# Tracking variables
index = 0
# Tracking variables and collectors
updates = []
tags = set()
paths = set()
last_path = None
arg_index = 0
parsing_paths = True
tags_collected = set()
paths_collected = set()

# Go through the parsed arguments and pair up tags with paths
# Also perform some simple validations along the way
while index < len(parsed.arguments):
arg = parsed.arguments[index]
if parsing_paths:
if arg.startswith('@'):
if not paths:
parser.error("no paths given before '{}'".format(arg))
parsing_paths = False
elif not os.path.isdir(arg):
parser.error("invalid directory path '{}'".format(arg))
else:
last_path = arg
paths.add(arg)
index += 1
# Iterate through the arguments and pair up tags with paths
while arg_index < len(parsed.arguments):
arg = parsed.arguments[arg_index]
if parsing_paths and arg.startswith('@'):
if len(paths_collected) == 0:
parser.error("expecting paths before {}".format(arg))
parsing_paths = False
elif parsing_paths and not arg.startswith('@'):
paths_collected.add(arg)
arg_index += 1
elif not parsing_paths and arg.startswith('@'):
tag_name_has_alphabet = False
for ch in arg[1:]:
if ch not in TAG_NAME_CHARS:
parser.error("bad char {} in tag name {}".format(ch, arg))
tag_name_has_alphabet |= ch.isalpha()
if not tag_name_has_alphabet:
parser.error("no alphabets in tag name {}".format(arg))
tags_collected.add(arg)
arg_index += 1
else:
if arg.startswith('@'):
tag = arg[1:]
if len(tag) == 0:
parser.error("empty tag name '@'")
has_alpha = False
for char in tag:
if char not in TAG_NAME_CHARS:
parser.error(
"invalid character '{}' in tag name '@{}'"
.format(char, tag)
)
has_alpha |= char.isalpha()
if not has_alpha:
parser.error(
"no alphabets in tag name '@{}'".format(tag)
)
tags.add(arg)
index += 1
else:
updates.append((tags, paths))
tags, paths = set(), set()
parsing_paths = True
updates.append((tags_collected, paths_collected))
tags_collected, paths_collected = set(), set()
parsing_paths = True
if parsing_paths:
parser.error("expecting tags after '{}'".format(last_path))
updates.append((tags, paths))
parser.error("expecting a tag name")
updates.append((tags_collected, paths_collected))

# Save the new changes and print messages
# Apply updates and message
messages = set()
for tags, paths in updates:
valid_paths = [p for p in paths if os.path.isdir(p)]
if len(valid_paths) == 0:
continue
for tag in tags:
if tag not in tag_to_paths:
tag_to_paths[tag] = {}
for path in paths:
expanded_path = expand_path(path)
if not parsed.expand:
path = shrink_path(path)
tag_to_paths[tag][expanded_path] = path
messages.add(update_msg.format(tag, path))
save_config(tag_to_paths)
print("\n".join(messages))
for path in valid_paths:
full_path = expand_path(path)
short_path = shrink_path(path)
tag_to_paths[tag][full_path] = short_path
messages.add(msg.format(tag, full_path))
save_tags(tag_to_paths)
if messages:
print("\n".join(messages))
Loading

0 comments on commit a98512f

Please sign in to comment.